diff --git a/.github/workflows/lib-checks.yml b/.github/workflows/lib-checks.yml index f6d41cd1fb..580af70378 100644 --- a/.github/workflows/lib-checks.yml +++ b/.github/workflows/lib-checks.yml @@ -11,7 +11,7 @@ on: - ".github/scripts/**" - ".github/workflows/lib-checks.yml" - ".github/workflows/release_dev.yml" - - ".github/workflows/release_prod.yml.template" + - ".github/workflows/release.yml" - "LICENSE" - "README.rst" - "*.toml" @@ -73,7 +73,7 @@ jobs: file.startsWith('.github/scripts/') || file === '.github/workflows/lib-checks.yml' || file === '.github/workflows/release_dev.yml' || - file === '.github/workflows/release_prod.yml.template' + file === '.github/workflows/release.yml' ); const libraryChanged = files.some((file) => matchesLibrary(file.filename)); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a9ae22375c..8614672562 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,18 +3,51 @@ name: "Release" on: workflow_dispatch: inputs: + operation: + type: choice + description: "Release phase to run" + required: true + options: + - prepare + - publish + - finalize version: type: string - description: "Version number to release (e.g., 1.2.3, 1.2.3rc1, 1.2.0)" + description: "Canonical production version (for example, 2.9.0 or 2.9.0rc1)" + required: true + sync_readthedocs: + type: boolean + description: "Finalize: sync and activate the matching Read the Docs version" + required: true + default: true + close_milestone: + type: boolean + description: "Finalize: idempotently close the exact release milestone" required: true + default: true + notify_discord: + type: boolean + description: "Finalize: send Discord notification (not idempotent; default off)" + required: true + default: false + +permissions: {} -permissions: - contents: read +concurrency: + group: pycord-production-release + cancel-in-progress: false jobs: - safety_check: + prepare: + name: "Prepare committed release state" + if: ${{ inputs.operation == 'prepare' }} runs-on: ubuntu-latest - environment: release + environment: + name: release + permissions: + contents: read + env: + VERSION: ${{ inputs.version }} steps: - name: "Security Check" uses: Pycord-Development/execute-whitelist-action@107fcb23ce15f46d7fa11ffceb0d803140d7f220 # v2.2.0 @@ -22,337 +55,474 @@ jobs: whitelisted-github-ids: ${{ vars.ALLOWED_USER_IDS }} token: ${{ secrets.GITHUB_TOKEN }} - pre_config: - needs: [safety_check] - outputs: - branch_name: ${{ steps.determine_vars.outputs.branch_name }} - is_rc: ${{ steps.determine_vars.outputs.is_rc }} - version: ${{ steps.determine_vars.outputs.version }} - previous_tag: ${{ steps.determine_vars.outputs.previous_tag }} - previous_final_tag: ${{ steps.determine_vars.outputs.previous_final_tag }} - runs-on: ubuntu-latest - steps: - - name: "Checkout Repository" + # ADMIN_GITHUB_TOKEN authenticates as NyuwBot. The NyuwBot ruleset team is + # the explicit bypass actor for protected master and version-branch writes. + - name: "Checkout Repository as NyuwBot" uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + token: ${{ secrets.ADMIN_GITHUB_TOKEN }} + persist-credentials: true fetch-depth: 0 fetch-tags: true - - name: "Determine Push Branch" - id: determine_vars + - name: "Setup Node.js" + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24" + + - name: "Validate and derive production release values" + id: release + run: >- + node .github/scripts/release-tools.mjs derive + --channel production + --version "$VERSION" + + - name: "Require a branch dispatch and verify checkout" env: - VERSION: ${{ github.event.inputs.version }} + EXPECTED_COMMIT: ${{ github.sha }} + REF_TYPE: ${{ github.ref_type }} + BRANCH: ${{ github.ref_name }} run: | set -euo pipefail - VALID_VERSION_REGEX='^[0-9]+\.[0-9]+\.[0-9]+(rc[0-9]+)?$' - if ! [[ $VERSION =~ $VALID_VERSION_REGEX ]]; then - echo "::error::Invalid version string '$VERSION'. Only releases like 1.2.3 and release candidates like 1.2.3rc1 are supported." + test "$REF_TYPE" = "branch" + git check-ref-format --branch "$BRANCH" + test "$(git rev-parse HEAD)" = "$EXPECTED_COMMIT" + test -z "$(git status --porcelain)" + + - name: "Require the NyuwBot automation identity" + env: + ADMIN_GITHUB_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} + run: >- + node .github/scripts/release-tools.mjs check-github-identity + --expected-login NyuwBot + --token-env ADMIN_GITHUB_TOKEN + + - name: "Derive previous production tags" + id: history + run: >- + node .github/scripts/release-tools.mjs release-history + --repository "$GITHUB_WORKSPACE" + --version "$VERSION" + + - name: "Prepare production changelog" + env: + PREVIOUS_TAG: ${{ steps.history.outputs.previous_tag }} + PREVIOUS_FINAL_TAG: ${{ steps.history.outputs.previous_final_tag }} + BRANCH: ${{ github.ref_name }} + run: >- + node .github/scripts/release-tools.mjs update-changelog + --path CHANGELOG.md + --version "$VERSION" + --previous-tag "$PREVIOUS_TAG" + --previous-final-tag "$PREVIOUS_FINAL_TAG" + --branch "$BRANCH" + --repository "$GITHUB_REPOSITORY" + + - name: "Commit prepared changelog as NyuwBot" + run: | + set -euo pipefail + git config user.name "NyuwBot" + git config user.email "nyuw@aitsys.dev" + git add -- CHANGELOG.md + git diff --cached --exit-code && { + echo "::error::The changelog helper produced no committed change." exit 1 - fi - echo "version=$VERSION" >> $GITHUB_OUTPUT - PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || git describe --tags --abbrev=0 2>/dev/null || true) - if [[ -z "$PREVIOUS_TAG" ]]; then - echo "::error::Could not determine previous tag. Ensure at least one tag exists." + } + git commit -m "chore(release): update CHANGELOG.md for version $VERSION" + + - name: "Update version branch with an explicit lease" + env: + VERSION_BRANCH: ${{ steps.release.outputs.version_branch }} + run: | + set -euo pipefail + git check-ref-format --branch "$VERSION_BRANCH" + remote_line="$(git ls-remote --heads origin "refs/heads/$VERSION_BRANCH")" + if [[ "$(printf '%s\n' "$remote_line" | sed '/^$/d' | wc -l)" -gt 1 ]]; then + echo "::error::Remote returned ambiguous state for refs/heads/$VERSION_BRANCH." exit 1 fi - echo "previous_tag=${PREVIOUS_TAG}" >> $GITHUB_OUTPUT - PREVIOUS_FINAL_TAG=$(git tag --sort=-v:refname | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 || true) - if [[ -z "$PREVIOUS_FINAL_TAG" ]]; then - PREVIOUS_FINAL_TAG=$PREVIOUS_TAG - fi - echo "previous_final_tag=${PREVIOUS_FINAL_TAG}" >> $GITHUB_OUTPUT - MAJOR_MINOR_VERSION=$(echo $VERSION | grep -oE '^[0-9]+\.[0-9]+') - echo "branch_name=v${MAJOR_MINOR_VERSION}.x" >> $GITHUB_OUTPUT - if [[ $VERSION == *rc* ]]; then - echo "is_rc=true" >> $GITHUB_OUTPUT - else - echo "is_rc=false" >> $GITHUB_OUTPUT - fi + remote_sha="${remote_line%%[[:space:]]*}" + git push \ + --force-with-lease="refs/heads/$VERSION_BRANCH:$remote_sha" \ + origin "HEAD:refs/heads/$VERSION_BRANCH" - # branch_protection_rename: - # needs: [safety_check] - # runs-on: ubuntu-latest - # environment: release - # env: - # GH_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} - # outputs: - # master_rule_id: ${{ steps.get_rule_ids.outputs.master_rule_id }} - # master_pattern: ${{ steps.get_rule_ids.outputs.master_pattern }} - # v_rule_id: ${{ steps.get_rule_ids.outputs.v_rule_id }} - # v_pattern: ${{ steps.get_rule_ids.outputs.v_pattern }} - # steps: - # - name: Set up GitHub CLI - # uses: wusatosi/setup-gh@v1.1 - # with: - # token: ${{ secrets.ADMIN_GITHUB_TOKEN }} - # - name: Get branch protection rule IDs - # id: get_rule_ids - # run: | - # gh api repos/${{ github.repository }}/branch-protection-rules > rules.json - # MASTER_ID=$(jq -r '.[] | select(.pattern == "master") | .id' rules.json) - # MASTER_PATTERN=$(jq -r '.[] | select(.pattern == "master") | .pattern' rules.json) - # V_ID=$(jq -r '.[] | select(.pattern | test("^v[0-9]+\\.[0-9]+\\.x$")) | .id' rules.json) - # V_PATTERN=$(jq -r '.[] | select(.pattern | test("^v[0-9]+\\.[0-9]+\\.x$")) | .pattern' rules.json) - # echo "master_rule_id=$MASTER_ID" >> $GITHUB_OUTPUT - # echo "master_pattern=$MASTER_PATTERN" >> $GITHUB_OUTPUT - # echo "v_rule_id=$V_ID" >> $GITHUB_OUTPUT - # echo "v_pattern=$V_PATTERN" >> $GITHUB_OUTPUT - # - name: Rename master protection to temp-master - # if: ${{ steps.get_rule_ids.outputs.master_rule_id != '' }} - # run: | - # gh api repos/${{ github.repository }}/branch-protection-rules/${{ steps.get_rule_ids.outputs.master_rule_id }} \ - # -X PATCH -F pattern="temp-master" - # - name: Rename v*.* protection to temp-v - # if: ${{ steps.get_rule_ids.outputs.v_rule_id != '' }} - # run: | - # gh api repos/${{ github.repository }}/branch-protection-rules/${{ steps.get_rule_ids.outputs.v_rule_id }} \ - # -X PATCH -F pattern="temp-v" - - lib_release: - needs: [pre_config] # , branch_protection_rename + - name: "Push prepared commit to the dispatch branch" + env: + BRANCH: ${{ github.ref_name }} + run: git push origin "HEAD:refs/heads/$BRANCH" + + publish: + name: "Build, attest, and publish py-cord" + if: ${{ inputs.operation == 'publish' }} runs-on: ubuntu-latest - environment: release + environment: + name: release + url: https://pypi.org/p/py-cord + permissions: + contents: write + id-token: write + attestations: write env: - GITHUB_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} + VERSION: ${{ inputs.version }} steps: + - name: "Security Check" + uses: Pycord-Development/execute-whitelist-action@107fcb23ce15f46d7fa11ffceb0d803140d7f220 # v2.2.0 + with: + whitelisted-github-ids: ${{ vars.ALLOWED_USER_IDS }} + token: ${{ secrets.GITHUB_TOKEN }} + - name: "Checkout Repository" uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 fetch-tags: true - - name: "Create version branch if missing" - id: conditional-create-version-branch - shell: bash - env: - VERSION_BRANCH: ${{ needs.pre_config.outputs.branch_name }} - GITHUB_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} - run: | - git fetch origin - if ! git show-ref --verify --quiet refs/heads/$VERSION_BRANCH; then - git checkout -b $VERSION_BRANCH - git push origin $VERSION_BRANCH -f - fi - git checkout $VERSION_BRANCH - - name: "Setup Python" - id: python-setup - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + + - name: "Setup Node.js" + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - python-version: "3.14" - cache: "pip" - cache-dependency-path: "requirements/_release.txt" - - name: "Install Release Dependencies" - id: python-install - env: - REQ_FILE: "requirements/_release.txt" - shell: bash - run: | - python -m pip install --upgrade pip - pip install setuptools setuptools_scm twine build - pip install -r $REQ_FILE - - name: "Prepare and Update CHANGELOG.md" - id: changelog-update - shell: bash + node-version: "24" + + - name: "Validate and derive production release values" + id: release + run: >- + node .github/scripts/release-tools.mjs derive + --channel production + --version "$VERSION" + + - name: "Verify prepared source commit" env: - VERSION: ${{ inputs.version }} - PREVIOUS_TAG: ${{ needs.pre_config.outputs.previous_tag }} - PREVIOUS_FINAL_TAG: ${{ needs.pre_config.outputs.previous_final_tag }} - REPOSITORY: ${{ github.repository }} - GITHUB_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} - BRANCH: ${{ github.ref_name }} + EXPECTED_COMMIT: ${{ github.sha }} run: | - git config user.name "NyuwBot" - git config user.email "nyuw@aitsys.dev" - DATE=$(date +'%Y-%m-%d') - python scripts/release_changelog.py \ + set -euo pipefail + test "$(git rev-parse HEAD)" = "$EXPECTED_COMMIT" + test -z "$(git status --porcelain)" + node .github/scripts/release-tools.mjs check-changelog \ --path CHANGELOG.md \ --version "$VERSION" \ - --previous-tag "$PREVIOUS_TAG" \ - --previous-final-tag "$PREVIOUS_FINAL_TAG" \ - --branch "$BRANCH" \ - --repository "$REPOSITORY" \ - --date "$DATE" - git add CHANGELOG.md - git commit -m "chore(release): update CHANGELOG.md for version $VERSION" - - name: "Commit and Push Changelog to ${{ github.ref_name }}" - id: commit-main-branch - shell: bash + --repository "$GITHUB_REPOSITORY" + + - name: "Derive reproducible build timestamp" + id: source_date + run: >- + node .github/scripts/release-tools.mjs source-date-epoch + --repository "$GITHUB_WORKSPACE" + --commit "$GITHUB_SHA" + + - name: "Require immutable GitHub releases" env: - VERSION: ${{ inputs.version }} - GITHUB_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} - BRANCH: ${{ github.ref_name }} + GH_TOKEN: ${{ github.token }} + IMMUTABLE_STATE: ${{ runner.temp }}/immutable-releases.json run: | - git config user.name "NyuwBot" - git config user.email "nyuw@aitsys.dev" - git push origin HEAD:$BRANCH -f - - name: "Push Changelog to Version Branch" - id: commit-version-branch - shell: bash + gh api "repos/$GITHUB_REPOSITORY/immutable-releases" > "$IMMUTABLE_STATE" + node .github/scripts/release-tools.mjs check-immutable --state-file "$IMMUTABLE_STATE" + + - name: "Require an unused PyPI production version" + run: >- + node .github/scripts/release-tools.mjs check-pypi-unused + --project py-cord + --version "$VERSION" + + - name: "Setup uv" + uses: ./.github/actions/setup-uv + with: + python-version: "3.14" + groups: "release" + frozen: "true" + + - name: "Build wheel and source distribution" env: - GITHUB_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} - VERSION_BRANCH: ${{ needs.pre_config.outputs.branch_name }} - run: | - git config user.name "NyuwBot" - git config user.email "nyuw@aitsys.dev" - git push origin HEAD:$VERSION_BRANCH -f - - name: "Create Git Tag" - id: create-git-tag - shell: bash + SETUPTOOLS_SCM_PRETEND_VERSION: ${{ steps.release.outputs.version }} + SOURCE_DATE_EPOCH: ${{ steps.source_date.outputs.source_date_epoch }} + run: uv build --no-sources --clear --no-create-gitignore + + - name: "Validate production distributions" + id: artifacts + run: >- + node .github/scripts/release-tools.mjs validate-artifacts + --channel production + --version "$VERSION" + --dist-dir "$GITHUB_WORKSPACE/dist" + + - name: "Validate distribution metadata" + run: twine check --strict "${{ steps.artifacts.outputs.wheel_path }}" "${{ steps.artifacts.outputs.sdist_path }}" + + - name: "Install and import the built wheel" env: - VERSION: ${{ inputs.version }} - GITHUB_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} - run: | - git config user.name "NyuwBot" - git config user.email "nyuw@aitsys.dev" - git tag v$VERSION -m "Release version $VERSION" - git push origin v$VERSION -f - - name: "Verify Version" - id: python-version-verify - shell: bash - run: python -m setuptools_scm - - name: "Build Package" - id: python-version-build - shell: bash + EXPECTED_VERSION: ${{ steps.release.outputs.version }} + WHEEL_PATH: ${{ steps.artifacts.outputs.wheel_path }} + VERIFY_ENV: ${{ runner.temp }}/pycord-prod-verify run: | - python3 -m build --sdist - python3 -m build --wheel - - name: "Create GitHub Release" - uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 - id: gh-release + uv venv --clear --python 3.14 "$VERIFY_ENV" + uv pip install --python "$VERIFY_ENV/bin/python" "$WHEEL_PATH" + cd "$VERIFY_ENV" + "$VERIFY_ENV/bin/python" -c 'import importlib.metadata as metadata, os; import discord; expected = os.environ["EXPECTED_VERSION"]; assert metadata.version("py-cord") == expected; assert discord.__version__ == expected' + + - name: "Generate build provenance attestations" + id: attest + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.0.0 with: - tag_name: "v${{ inputs.version }}" - name: "v${{ inputs.version }}" - generate_release_notes: true - draft: false - prerelease: ${{ needs.pre_config.outputs.is_rc }} - files: | - dist/*.whl - dist/*.tar.gz - token: ${{ secrets.ADMIN_GITHUB_TOKEN }} - make_latest: true - repository: ${{ github.repository }} - target_commitish: ${{ github.ref_name }} + subject-path: | + ${{ steps.artifacts.outputs.wheel_path }} + ${{ steps.artifacts.outputs.sdist_path }} - - name: "Publish package distributions to PyPI" - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + - name: "Verify wheel provenance" + env: + GH_TOKEN: ${{ github.token }} + run: >- + gh attestation verify "${{ steps.artifacts.outputs.wheel_path }}" + --bundle "${{ steps.attest.outputs.bundle-path }}" + --repo "$GITHUB_REPOSITORY" + --signer-workflow "$GITHUB_REPOSITORY/.github/workflows/release.yml" + --source-digest "$GITHUB_SHA" + --deny-self-hosted-runners + + - name: "Verify source distribution provenance" + env: + GH_TOKEN: ${{ github.token }} + run: >- + gh attestation verify "${{ steps.artifacts.outputs.sdist_path }}" + --bundle "${{ steps.attest.outputs.bundle-path }}" + --repo "$GITHUB_REPOSITORY" + --signer-workflow "$GITHUB_REPOSITORY/.github/workflows/release.yml" + --source-digest "$GITHUB_SHA" + --deny-self-hosted-runners + + - name: "Check production tag state" + id: tag + run: >- + node .github/scripts/release-tools.mjs check-tag + --repository "$GITHUB_WORKSPACE" + --remote origin + --tag "${{ steps.release.outputs.tag }}" + --expected-commit "$GITHUB_SHA" + + - name: "Check production release state" + id: release_state + env: + GITHUB_TOKEN: ${{ github.token }} + run: >- + node .github/scripts/release-tools.mjs check-github-release + --channel production + --version "$VERSION" + --expected-commit "$GITHUB_SHA" + --dist-dir "${{ steps.artifacts.outputs.dist_dir }}" + --repository "$GITHUB_REPOSITORY" + --tag-state "${{ steps.tag.outputs.tag_state }}" + + - name: "Create and push annotated production tag" + if: ${{ steps.tag.outputs.create_tag == 'true' }} + env: + TAG: ${{ steps.release.outputs.tag }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag --annotate "$TAG" "$GITHUB_SHA" --message "Release $VERSION" + git push origin "refs/tags/$TAG" + + - name: "Verify final production tag state" + id: final_tag + run: >- + node .github/scripts/release-tools.mjs check-tag + --repository "$GITHUB_WORKSPACE" + --remote origin + --tag "${{ steps.release.outputs.tag }}" + --expected-commit "$GITHUB_SHA" + + - name: "Create immutable GitHub release" + if: ${{ steps.release_state.outputs.create_release == 'true' }} + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.release.outputs.tag }} + TITLE: ${{ steps.release.outputs.title }} + PRERELEASE: ${{ steps.release.outputs.prerelease }} + LATEST: ${{ steps.release.outputs.latest }} + run: >- + gh release create "$TAG" + "${{ steps.artifacts.outputs.wheel_path }}" + "${{ steps.artifacts.outputs.sdist_path }}" + --repo "$GITHUB_REPOSITORY" + --verify-tag + --target "$GITHUB_SHA" + --title "$TITLE" + --generate-notes + --prerelease="$PRERELEASE" + --latest="$LATEST" + + - name: "Upload missing wheel to interrupted draft" + if: ${{ steps.release_state.outputs.resume_draft == 'true' && steps.release_state.outputs.wheel_missing == 'true' }} + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.release.outputs.tag }} + run: gh release upload "$TAG" "${{ steps.artifacts.outputs.wheel_path }}" --repo "$GITHUB_REPOSITORY" + + - name: "Upload missing source distribution to interrupted draft" + if: ${{ steps.release_state.outputs.resume_draft == 'true' && steps.release_state.outputs.sdist_missing == 'true' }} + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.release.outputs.tag }} + run: gh release upload "$TAG" "${{ steps.artifacts.outputs.sdist_path }}" --repo "$GITHUB_REPOSITORY" + + - name: "Publish interrupted draft" + if: ${{ steps.release_state.outputs.resume_draft == 'true' }} + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.release.outputs.tag }} + TITLE: ${{ steps.release.outputs.title }} + PRERELEASE: ${{ steps.release.outputs.prerelease }} + LATEST: ${{ steps.release.outputs.latest }} + run: >- + gh release edit "$TAG" + --repo "$GITHUB_REPOSITORY" + --verify-tag + --target "$GITHUB_SHA" + --title "$TITLE" + --draft=false + --prerelease="$PRERELEASE" + --latest="$LATEST" + + - name: "Verify immutable release and assets" env: - name: "pypi" - url: "https://pypi.org/p/py-cord" + GITHUB_TOKEN: ${{ github.token }} + run: >- + node .github/scripts/release-tools.mjs check-github-release + --channel production + --version "$VERSION" + --expected-commit "$GITHUB_SHA" + --dist-dir "${{ steps.artifacts.outputs.dist_dir }}" + --repository "$GITHUB_REPOSITORY" + --tag-state "${{ steps.final_tag.outputs.tag_state }}" + + - name: "Publish distributions to PyPI" + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 with: - password: ${{ secrets.PYPI_TOKEN }} - user: __token__ - attestations: false - verify-metadata: false + packages-dir: dist/ - - name: "Echo release url" - run: echo "${{ steps.gh-release.outputs.url }}" + # This phase intentionally ends after trusted publishing. - docs_release: + finalize: + name: "Finalize published release" + if: ${{ inputs.operation == 'finalize' }} runs-on: ubuntu-latest - needs: [lib_release, pre_config] - environment: release + environment: + name: release + permissions: + contents: read + issues: write + env: + VERSION: ${{ inputs.version }} steps: - - name: "Checkout repository" + - name: "Security Check" + uses: Pycord-Development/execute-whitelist-action@107fcb23ce15f46d7fa11ffceb0d803140d7f220 # v2.2.0 + with: + whitelisted-github-ids: ${{ vars.ALLOWED_USER_IDS }} + token: ${{ secrets.GITHUB_TOKEN }} + + - name: "Checkout Repository" uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 fetch-tags: true - - name: "Sync and activate version on Read the Docs" + - name: "Setup Node.js" + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24" + + - name: "Validate and derive production release values" + id: release + run: >- + node .github/scripts/release-tools.mjs derive + --channel production + --version "$VERSION" + + - name: "Derive previous production tags" + id: history + run: >- + node .github/scripts/release-tools.mjs release-history + --repository "$GITHUB_WORKSPACE" + --version "$VERSION" + + - name: "Resolve published annotated tag" + id: tag + run: >- + node .github/scripts/release-tools.mjs resolve-tag + --repository "$GITHUB_WORKSPACE" + --remote origin + --tag "${{ steps.release.outputs.tag }}" + + - name: "Download immutable release assets" env: - READTHEDOCS_TOKEN: ${{ secrets.READTHEDOCS_TOKEN }} + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.release.outputs.tag }} + WHEEL: ${{ steps.release.outputs.wheel_name }} + SDIST: ${{ steps.release.outputs.sdist_name }} run: | - python3 scripts/release_rtd_versions.py \ - --project pycord \ - --version "${{ needs.pre_config.outputs.version }}" \ - --sync - - #inform_discord: - # runs-on: ubuntu-latest - # needs: [docs_release, lib_release, pre_config] - # environment: release - # steps: - # - name: "Checkout repository" - # uses: actions/checkout@v6 - # with: - # fetch-depth: 0 - # fetch-tags: true - - # - name: "Notify Discord" - # env: - # VERSION: ${{ needs.pre_config.outputs.version }} - # PREVIOUS_TAG: ${{ needs.pre_config.outputs.previous_tag }} - # PREVIOUS_FINAL_TAG: ${{ needs.pre_config.outputs.previous_final_tag }} - # DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} - # REPOSITORY: ${{ github.repository }} - # run: python scripts/discord_release_notification.py - - determine_milestone_id: - runs-on: ubuntu-latest - needs: [lib_release, pre_config] - if: ${{ !contains(needs.pre_config.outputs.version, '-') && false }} - outputs: - old_milestone_version: ${{ steps.extract_version.outputs.old_milestone_version }} - new_milestone_version: ${{ steps.extract_version.outputs.new_milestone_version }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - environment: release - steps: - - name: "Extract Milestone Version" - id: extract_version - run: | - gh extension install valeriobelli/gh-milestone - VERSION=${{ needs.pre_config.outputs.version }} - PREV_MAJOR_MINOR=$(echo $VERSION | awk -F. '{printf "v%d.%d", $1, $2-1}') - OLD_MILESTONE_VERSION=$(gh milestone list --query "$PREV_MAJOR_MINOR" | grep "#" | awk '{print $2}') - NEW_MILESTONE_VERSION="v$(echo $VERSION | grep -oE '^[0-9]+\.[0-9]+')" - echo "old_milestone_version=$OLD_MILESTONE_VERSION" >> $GITHUB_OUTPUT - echo "new_milestone_version=$NEW_MILESTONE_VERSION" >> $GITHUB_OUTPUT - -# close_milestone: -# runs-on: ubuntu-latest -# needs: [determine_milestone_id, pre_config] -# if: ${{ !contains(needs.pre_config.outputs.version, 'rc') && -# endsWith(needs.pre_config.outputs.version, '.0') && false }} -# environment: release -# env: -# GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} -# steps: -# - name: "Checkout Repository" -# uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 -# with: -# fetch-depth: 0 -# fetch-tags: true -# -# - name: "Close Milestone" -# run: | -# gh extension install valeriobelli/gh-milestone -# OLD_MILESTONE_ID=$(gh milestone list --query "${{ needs.determine_milestone_id.outputs.old_milestone_version }}" | grep "#" | cut -d' ' -f2 | cut -d '#' -f2) -# gh milestone edit $OLD_MILESTONE_ID --state closed -# -# - name: "Create New Milestone" -# run: | -# gh extension install valeriobelli/gh-milestone -# gh milestone create -t "${{ needs.determine_milestone_id.outputs.new_milestone_version }}" - -# branch_protection_restore: -# runs-on: ubuntu-latest -# needs: [branch_protection_rename, lib_release, docs_release, inform_discord, determine_milestone_id, close_milestone] -# environment: release -# if: always() -# env: -# GH_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} -# steps: -# - name: Set up GitHub CLI -# uses: wusatosi/setup-gh@v1.1 -# with: -# token: ${{ secrets.ADMIN_GITHUB_TOKEN }} -# - name: Restore master protection pattern -# if: ${{ needs.branch_protection_rename.outputs.master_rule_id != '' }} -# run: | -# gh api repos/${{ github.repository }}/branch-protection-rules/${{ needs.branch_protection_rename.outputs.master_rule_id }} \ -# -X PATCH -F pattern="${{ needs.branch_protection_rename.outputs.master_pattern }}" -# - name: Restore v*.* protection pattern -# if: ${{ needs.branch_protection_rename.outputs.v_rule_id != '' }} -# run: | -# gh api repos/${{ github.repository }}/branch-protection-rules/${{ needs.branch_protection_rename.outputs.v_rule_id }} \ -# -X PATCH -F pattern="${{ needs.branch_protection_rename.outputs.v_pattern }}" + set -euo pipefail + test ! -e "$GITHUB_WORKSPACE/dist" + mkdir "$GITHUB_WORKSPACE/dist" + gh release download "$TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --pattern "$WHEEL" \ + --pattern "$SDIST" \ + --dir "$GITHUB_WORKSPACE/dist" + + - name: "Validate downloaded distributions" + id: artifacts + run: >- + node .github/scripts/release-tools.mjs validate-artifacts + --channel production + --version "$VERSION" + --dist-dir "$GITHUB_WORKSPACE/dist" + + - name: "Verify immutable GitHub release and assets" + env: + GITHUB_TOKEN: ${{ github.token }} + run: >- + node .github/scripts/release-tools.mjs check-github-release + --channel production + --version "$VERSION" + --expected-commit "${{ steps.tag.outputs.tag_commit }}" + --dist-dir "${{ steps.artifacts.outputs.dist_dir }}" + --repository "$GITHUB_REPOSITORY" + --tag-state "${{ steps.tag.outputs.tag_state }}" + + - name: "Verify exact PyPI publication" + run: >- + node .github/scripts/release-tools.mjs check-pypi-published + --project py-cord + --channel production + --version "$VERSION" + --dist-dir "${{ steps.artifacts.outputs.dist_dir }}" + + - name: "Sync and activate Read the Docs version" + if: ${{ inputs.sync_readthedocs }} + env: + READTHEDOCS_TOKEN: ${{ secrets.READTHEDOCS_TOKEN }} + run: >- + node .github/scripts/release-tools.mjs rtd-release + --project pycord + --version "$VERSION" + --sync true + --attempts 24 + + - name: "Close exact release milestone" + if: ${{ inputs.close_milestone }} + env: + GITHUB_TOKEN: ${{ github.token }} + run: >- + node .github/scripts/release-tools.mjs close-milestone + --repository "$GITHUB_REPOSITORY" + --version "$VERSION" + + - name: "Notify Discord" + if: ${{ inputs.notify_discord }} + env: + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + PREVIOUS_TAG: ${{ steps.history.outputs.previous_tag }} + PREVIOUS_FINAL_TAG: ${{ steps.history.outputs.previous_final_tag }} + run: >- + node .github/scripts/release-tools.mjs notify-discord + --version "$VERSION" + --previous-tag "$PREVIOUS_TAG" + --previous-final-tag "$PREVIOUS_FINAL_TAG" + --repository "$GITHUB_REPOSITORY" diff --git a/.github/workflows/release_prod.yml.template b/.github/workflows/release_prod.yml.template deleted file mode 100644 index 726f1c8031..0000000000 --- a/.github/workflows/release_prod.yml.template +++ /dev/null @@ -1,538 +0,0 @@ -# DISABLED PRODUCTION TEMPLATE -# -# GitHub ignores this file because it does not end in .yml or .yaml. -# To activate it, review it after the development rehearsal and replace -# .github/workflows/release.yml with this file's contents. -# -# Do not deploy this as release_prod.yml. The py-cord trusted publisher on PyPI -# is bound to repository Pycord-Development/pycord, environment release, and -# the exact workflow filename release.yml. - -name: "Release" - -on: - workflow_dispatch: - inputs: - operation: - type: choice - description: "Release phase to run" - required: true - options: - - prepare - - publish - - finalize - version: - type: string - description: "Canonical production version (for example, 2.9.0 or 2.9.0rc1)" - required: true - sync_readthedocs: - type: boolean - description: "Finalize: sync and activate the matching Read the Docs version" - required: true - default: true - close_milestone: - type: boolean - description: "Finalize: idempotently close the exact release milestone" - required: true - default: true - notify_discord: - type: boolean - description: "Finalize: send Discord notification (not idempotent; default off)" - required: true - default: false - -permissions: {} - -concurrency: - group: pycord-production-release - cancel-in-progress: false - -jobs: - prepare: - name: "Prepare committed release state" - if: ${{ inputs.operation == 'prepare' }} - runs-on: ubuntu-latest - environment: - name: release - permissions: - contents: read - env: - VERSION: ${{ inputs.version }} - steps: - - name: "Security Check" - uses: Pycord-Development/execute-whitelist-action@107fcb23ce15f46d7fa11ffceb0d803140d7f220 # v2.2.0 - with: - whitelisted-github-ids: ${{ vars.ALLOWED_USER_IDS }} - token: ${{ secrets.GITHUB_TOKEN }} - - # ADMIN_GITHUB_TOKEN authenticates as NyuwBot. The NyuwBot ruleset team is - # the explicit bypass actor for protected master and version-branch writes. - - name: "Checkout Repository as NyuwBot" - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - token: ${{ secrets.ADMIN_GITHUB_TOKEN }} - persist-credentials: true - fetch-depth: 0 - fetch-tags: true - - - name: "Setup Node.js" - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: "24" - - - name: "Validate and derive production release values" - id: release - run: >- - node .github/scripts/release-tools.mjs derive - --channel production - --version "$VERSION" - - - name: "Require a branch dispatch and verify checkout" - env: - EXPECTED_COMMIT: ${{ github.sha }} - REF_TYPE: ${{ github.ref_type }} - BRANCH: ${{ github.ref_name }} - run: | - set -euo pipefail - test "$REF_TYPE" = "branch" - git check-ref-format --branch "$BRANCH" - test "$(git rev-parse HEAD)" = "$EXPECTED_COMMIT" - test -z "$(git status --porcelain)" - - - name: "Require the NyuwBot automation identity" - env: - ADMIN_GITHUB_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} - run: >- - node .github/scripts/release-tools.mjs check-github-identity - --expected-login NyuwBot - --token-env ADMIN_GITHUB_TOKEN - - - name: "Derive previous production tags" - id: history - run: >- - node .github/scripts/release-tools.mjs release-history - --repository "$GITHUB_WORKSPACE" - --version "$VERSION" - - - name: "Prepare production changelog" - env: - PREVIOUS_TAG: ${{ steps.history.outputs.previous_tag }} - PREVIOUS_FINAL_TAG: ${{ steps.history.outputs.previous_final_tag }} - BRANCH: ${{ github.ref_name }} - run: >- - node .github/scripts/release-tools.mjs update-changelog - --path CHANGELOG.md - --version "$VERSION" - --previous-tag "$PREVIOUS_TAG" - --previous-final-tag "$PREVIOUS_FINAL_TAG" - --branch "$BRANCH" - --repository "$GITHUB_REPOSITORY" - - - name: "Commit prepared changelog as NyuwBot" - run: | - set -euo pipefail - git config user.name "NyuwBot" - git config user.email "nyuw@aitsys.dev" - git add -- CHANGELOG.md - git diff --cached --exit-code && { - echo "::error::The changelog helper produced no committed change." - exit 1 - } - git commit -m "chore(release): update CHANGELOG.md for version $VERSION" - - - name: "Update version branch with an explicit lease" - env: - VERSION_BRANCH: ${{ steps.release.outputs.version_branch }} - run: | - set -euo pipefail - git check-ref-format --branch "$VERSION_BRANCH" - remote_line="$(git ls-remote --heads origin "refs/heads/$VERSION_BRANCH")" - if [[ "$(printf '%s\n' "$remote_line" | sed '/^$/d' | wc -l)" -gt 1 ]]; then - echo "::error::Remote returned ambiguous state for refs/heads/$VERSION_BRANCH." - exit 1 - fi - remote_sha="${remote_line%%[[:space:]]*}" - git push \ - --force-with-lease="refs/heads/$VERSION_BRANCH:$remote_sha" \ - origin "HEAD:refs/heads/$VERSION_BRANCH" - - - name: "Push prepared commit to the dispatch branch" - env: - BRANCH: ${{ github.ref_name }} - run: git push origin "HEAD:refs/heads/$BRANCH" - - publish: - name: "Build, attest, and publish py-cord" - if: ${{ inputs.operation == 'publish' }} - runs-on: ubuntu-latest - environment: - name: release - url: https://pypi.org/p/py-cord - permissions: - contents: write - id-token: write - attestations: write - env: - VERSION: ${{ inputs.version }} - steps: - - name: "Security Check" - uses: Pycord-Development/execute-whitelist-action@107fcb23ce15f46d7fa11ffceb0d803140d7f220 # v2.2.0 - with: - whitelisted-github-ids: ${{ vars.ALLOWED_USER_IDS }} - token: ${{ secrets.GITHUB_TOKEN }} - - - name: "Checkout Repository" - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - fetch-tags: true - - - name: "Setup Node.js" - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: "24" - - - name: "Validate and derive production release values" - id: release - run: >- - node .github/scripts/release-tools.mjs derive - --channel production - --version "$VERSION" - - - name: "Verify prepared source commit" - env: - EXPECTED_COMMIT: ${{ github.sha }} - run: | - set -euo pipefail - test "$(git rev-parse HEAD)" = "$EXPECTED_COMMIT" - test -z "$(git status --porcelain)" - node .github/scripts/release-tools.mjs check-changelog \ - --path CHANGELOG.md \ - --version "$VERSION" \ - --repository "$GITHUB_REPOSITORY" - - - name: "Derive reproducible build timestamp" - id: source_date - run: >- - node .github/scripts/release-tools.mjs source-date-epoch - --repository "$GITHUB_WORKSPACE" - --commit "$GITHUB_SHA" - - - name: "Require immutable GitHub releases" - env: - GH_TOKEN: ${{ github.token }} - IMMUTABLE_STATE: ${{ runner.temp }}/immutable-releases.json - run: | - gh api "repos/$GITHUB_REPOSITORY/immutable-releases" > "$IMMUTABLE_STATE" - node .github/scripts/release-tools.mjs check-immutable --state-file "$IMMUTABLE_STATE" - - - name: "Require an unused PyPI production version" - run: >- - node .github/scripts/release-tools.mjs check-pypi-unused - --project py-cord - --version "$VERSION" - - - name: "Setup uv" - uses: ./.github/actions/setup-uv - with: - python-version: "3.14" - groups: "release" - frozen: "true" - - - name: "Build wheel and source distribution" - env: - SETUPTOOLS_SCM_PRETEND_VERSION: ${{ steps.release.outputs.version }} - SOURCE_DATE_EPOCH: ${{ steps.source_date.outputs.source_date_epoch }} - run: uv build --no-sources --clear --no-create-gitignore - - - name: "Validate production distributions" - id: artifacts - run: >- - node .github/scripts/release-tools.mjs validate-artifacts - --channel production - --version "$VERSION" - --dist-dir "$GITHUB_WORKSPACE/dist" - - - name: "Validate distribution metadata" - run: twine check --strict "${{ steps.artifacts.outputs.wheel_path }}" "${{ steps.artifacts.outputs.sdist_path }}" - - - name: "Install and import the built wheel" - env: - EXPECTED_VERSION: ${{ steps.release.outputs.version }} - WHEEL_PATH: ${{ steps.artifacts.outputs.wheel_path }} - VERIFY_ENV: ${{ runner.temp }}/pycord-prod-verify - run: | - uv venv --clear --python 3.14 "$VERIFY_ENV" - uv pip install --python "$VERIFY_ENV/bin/python" "$WHEEL_PATH" - cd "$VERIFY_ENV" - "$VERIFY_ENV/bin/python" -c 'import importlib.metadata as metadata, os; import discord; expected = os.environ["EXPECTED_VERSION"]; assert metadata.version("py-cord") == expected; assert discord.__version__ == expected' - - - name: "Generate build provenance attestations" - id: attest - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.0.0 - with: - subject-path: | - ${{ steps.artifacts.outputs.wheel_path }} - ${{ steps.artifacts.outputs.sdist_path }} - - - name: "Verify wheel provenance" - env: - GH_TOKEN: ${{ github.token }} - run: >- - gh attestation verify "${{ steps.artifacts.outputs.wheel_path }}" - --bundle "${{ steps.attest.outputs.bundle-path }}" - --repo "$GITHUB_REPOSITORY" - --signer-workflow "$GITHUB_REPOSITORY/.github/workflows/release.yml" - --source-digest "$GITHUB_SHA" - --deny-self-hosted-runners - - - name: "Verify source distribution provenance" - env: - GH_TOKEN: ${{ github.token }} - run: >- - gh attestation verify "${{ steps.artifacts.outputs.sdist_path }}" - --bundle "${{ steps.attest.outputs.bundle-path }}" - --repo "$GITHUB_REPOSITORY" - --signer-workflow "$GITHUB_REPOSITORY/.github/workflows/release.yml" - --source-digest "$GITHUB_SHA" - --deny-self-hosted-runners - - - name: "Check production tag state" - id: tag - run: >- - node .github/scripts/release-tools.mjs check-tag - --repository "$GITHUB_WORKSPACE" - --remote origin - --tag "${{ steps.release.outputs.tag }}" - --expected-commit "$GITHUB_SHA" - - - name: "Check production release state" - id: release_state - env: - GITHUB_TOKEN: ${{ github.token }} - run: >- - node .github/scripts/release-tools.mjs check-github-release - --channel production - --version "$VERSION" - --expected-commit "$GITHUB_SHA" - --dist-dir "${{ steps.artifacts.outputs.dist_dir }}" - --repository "$GITHUB_REPOSITORY" - --tag-state "${{ steps.tag.outputs.tag_state }}" - - - name: "Create and push annotated production tag" - if: ${{ steps.tag.outputs.create_tag == 'true' }} - env: - TAG: ${{ steps.release.outputs.tag }} - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git tag --annotate "$TAG" "$GITHUB_SHA" --message "Release $VERSION" - git push origin "refs/tags/$TAG" - - - name: "Verify final production tag state" - id: final_tag - run: >- - node .github/scripts/release-tools.mjs check-tag - --repository "$GITHUB_WORKSPACE" - --remote origin - --tag "${{ steps.release.outputs.tag }}" - --expected-commit "$GITHUB_SHA" - - - name: "Create immutable GitHub release" - if: ${{ steps.release_state.outputs.create_release == 'true' }} - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ steps.release.outputs.tag }} - TITLE: ${{ steps.release.outputs.title }} - PRERELEASE: ${{ steps.release.outputs.prerelease }} - LATEST: ${{ steps.release.outputs.latest }} - run: >- - gh release create "$TAG" - "${{ steps.artifacts.outputs.wheel_path }}" - "${{ steps.artifacts.outputs.sdist_path }}" - --repo "$GITHUB_REPOSITORY" - --verify-tag - --target "$GITHUB_SHA" - --title "$TITLE" - --generate-notes - --prerelease="$PRERELEASE" - --latest="$LATEST" - - - name: "Upload missing wheel to interrupted draft" - if: ${{ steps.release_state.outputs.resume_draft == 'true' && steps.release_state.outputs.wheel_missing == 'true' }} - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ steps.release.outputs.tag }} - run: gh release upload "$TAG" "${{ steps.artifacts.outputs.wheel_path }}" --repo "$GITHUB_REPOSITORY" - - - name: "Upload missing source distribution to interrupted draft" - if: ${{ steps.release_state.outputs.resume_draft == 'true' && steps.release_state.outputs.sdist_missing == 'true' }} - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ steps.release.outputs.tag }} - run: gh release upload "$TAG" "${{ steps.artifacts.outputs.sdist_path }}" --repo "$GITHUB_REPOSITORY" - - - name: "Publish interrupted draft" - if: ${{ steps.release_state.outputs.resume_draft == 'true' }} - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ steps.release.outputs.tag }} - TITLE: ${{ steps.release.outputs.title }} - PRERELEASE: ${{ steps.release.outputs.prerelease }} - LATEST: ${{ steps.release.outputs.latest }} - run: >- - gh release edit "$TAG" - --repo "$GITHUB_REPOSITORY" - --verify-tag - --target "$GITHUB_SHA" - --title "$TITLE" - --draft=false - --prerelease="$PRERELEASE" - --latest="$LATEST" - - - name: "Verify immutable release and assets" - env: - GITHUB_TOKEN: ${{ github.token }} - run: >- - node .github/scripts/release-tools.mjs check-github-release - --channel production - --version "$VERSION" - --expected-commit "$GITHUB_SHA" - --dist-dir "${{ steps.artifacts.outputs.dist_dir }}" - --repository "$GITHUB_REPOSITORY" - --tag-state "${{ steps.final_tag.outputs.tag_state }}" - - - name: "Publish distributions to PyPI" - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 - with: - packages-dir: dist/ - - # This phase intentionally ends after trusted publishing. - - finalize: - name: "Finalize published release" - if: ${{ inputs.operation == 'finalize' }} - runs-on: ubuntu-latest - environment: - name: release - permissions: - contents: read - issues: write - env: - VERSION: ${{ inputs.version }} - steps: - - name: "Security Check" - uses: Pycord-Development/execute-whitelist-action@107fcb23ce15f46d7fa11ffceb0d803140d7f220 # v2.2.0 - with: - whitelisted-github-ids: ${{ vars.ALLOWED_USER_IDS }} - token: ${{ secrets.GITHUB_TOKEN }} - - - name: "Checkout Repository" - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - fetch-tags: true - - - name: "Setup Node.js" - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: "24" - - - name: "Validate and derive production release values" - id: release - run: >- - node .github/scripts/release-tools.mjs derive - --channel production - --version "$VERSION" - - - name: "Derive previous production tags" - id: history - run: >- - node .github/scripts/release-tools.mjs release-history - --repository "$GITHUB_WORKSPACE" - --version "$VERSION" - - - name: "Resolve published annotated tag" - id: tag - run: >- - node .github/scripts/release-tools.mjs resolve-tag - --repository "$GITHUB_WORKSPACE" - --remote origin - --tag "${{ steps.release.outputs.tag }}" - - - name: "Download immutable release assets" - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ steps.release.outputs.tag }} - WHEEL: ${{ steps.release.outputs.wheel_name }} - SDIST: ${{ steps.release.outputs.sdist_name }} - run: | - set -euo pipefail - test ! -e "$GITHUB_WORKSPACE/dist" - mkdir "$GITHUB_WORKSPACE/dist" - gh release download "$TAG" \ - --repo "$GITHUB_REPOSITORY" \ - --pattern "$WHEEL" \ - --pattern "$SDIST" \ - --dir "$GITHUB_WORKSPACE/dist" - - - name: "Validate downloaded distributions" - id: artifacts - run: >- - node .github/scripts/release-tools.mjs validate-artifacts - --channel production - --version "$VERSION" - --dist-dir "$GITHUB_WORKSPACE/dist" - - - name: "Verify immutable GitHub release and assets" - env: - GITHUB_TOKEN: ${{ github.token }} - run: >- - node .github/scripts/release-tools.mjs check-github-release - --channel production - --version "$VERSION" - --expected-commit "${{ steps.tag.outputs.tag_commit }}" - --dist-dir "${{ steps.artifacts.outputs.dist_dir }}" - --repository "$GITHUB_REPOSITORY" - --tag-state "${{ steps.tag.outputs.tag_state }}" - - - name: "Verify exact PyPI publication" - run: >- - node .github/scripts/release-tools.mjs check-pypi-published - --project py-cord - --channel production - --version "$VERSION" - --dist-dir "${{ steps.artifacts.outputs.dist_dir }}" - - - name: "Sync and activate Read the Docs version" - if: ${{ inputs.sync_readthedocs }} - env: - READTHEDOCS_TOKEN: ${{ secrets.READTHEDOCS_TOKEN }} - run: >- - node .github/scripts/release-tools.mjs rtd-release - --project pycord - --version "$VERSION" - --sync true - --attempts 24 - - - name: "Close exact release milestone" - if: ${{ inputs.close_milestone }} - env: - GITHUB_TOKEN: ${{ github.token }} - run: >- - node .github/scripts/release-tools.mjs close-milestone - --repository "$GITHUB_REPOSITORY" - --version "$VERSION" - - - name: "Notify Discord" - if: ${{ inputs.notify_discord }} - env: - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} - PREVIOUS_TAG: ${{ steps.history.outputs.previous_tag }} - PREVIOUS_FINAL_TAG: ${{ steps.history.outputs.previous_final_tag }} - run: >- - node .github/scripts/release-tools.mjs notify-discord - --version "$VERSION" - --previous-tag "$PREVIOUS_TAG" - --previous-final-tag "$PREVIOUS_FINAL_TAG" - --repository "$GITHUB_REPOSITORY"