chore(deps): bump the npm-minor-patch group in /docs/site with 5 upda… #25
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
| name: Release | |
| on: | |
| push: | |
| branches: [main] | |
| # Serialise release runs. Two rapid pushes to main otherwise race: the second | |
| # force-updates the release PR branch while the first is mid-flight, and the | |
| # first's createCommitOnBranch fails its expectedHeadOid. Queue rather than | |
| # cancel -- cancelling a run part-way through publishing is worse than waiting. | |
| concurrency: | |
| # Deliberately not keyed on github.ref. This workflow only ever acts on the | |
| # default branch -- release-please's target-branch defaults to it regardless | |
| # of the ref the run started from -- so a ref-keyed group would put a run | |
| # started from anywhere else into a separate group and let it race the very | |
| # thing this block serialises. | |
| group: ${{ github.workflow }} | |
| cancel-in-progress: false | |
| permissions: {} | |
| jobs: | |
| release-please: | |
| runs-on: ubuntu-latest | |
| if: github.repository_owner == 'yo61' | |
| permissions: | |
| contents: read | |
| outputs: | |
| release_created: ${{ steps.rp.outputs.release_created }} | |
| tag_name: ${{ steps.rp.outputs.tag_name }} | |
| steps: | |
| # Mint a short-lived App token so the Release PR is authored by the App | |
| # rather than github-actions[bot]. PRs opened with the default | |
| # GITHUB_TOKEN do not fire `pull_request` workflows (GitHub's | |
| # loop-prevention policy), so the PR would have no checks and branch | |
| # protection would block the merge. | |
| - id: create_token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ secrets.SEMANTIC_RELEASE_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.SEMANTIC_RELEASE_APP_PRIVATE_KEY }} | |
| owner: ${{ github.repository_owner }} | |
| repositories: ${{ github.event.repository.name }} | |
| permission-contents: write | |
| permission-pull-requests: write | |
| - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 | |
| id: rp | |
| with: | |
| config-file: release-please-config.json | |
| manifest-file: .release-please-manifest.json | |
| token: ${{ steps.create_token.outputs.token }} | |
| sync-lockfile: | |
| needs: release-please | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| steps: | |
| - id: create_token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ secrets.SEMANTIC_RELEASE_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.SEMANTIC_RELEASE_APP_PRIVATE_KEY }} | |
| owner: ${{ github.repository_owner }} | |
| repositories: ${{ github.event.repository.name }} | |
| permission-contents: write | |
| permission-pull-requests: read | |
| # Resolve the Release PR from the open PR list, not from release-please's | |
| # `prs` output. That output is set only when release-please actually | |
| # updated a PR, and it declines to update when the regenerated body is | |
| # unchanged (manifest.ts, maybeUpdateExistingPullRequest). A sync missed | |
| # once -- a transient failure, or this job not existing yet -- would then | |
| # never be retried until the next releasable commit arrived. "Is there an | |
| # open Release PR?" is the precondition this job actually cares about. | |
| # | |
| # Three details, each of which was a bug before it was a comment: | |
| # | |
| # 1. `gh pr list --label` is NOT used. It resolves through `query | |
| # PullRequestSearch` -- the search API -- which is index-lagged and | |
| # can miss a PR release-please created seconds earlier in the | |
| # previous job. The unfiltered list hits repository.pullRequests | |
| # and is read-your-writes. | |
| # 2. --limit 201. The default is 30, ordered CREATED_AT DESC. The | |
| # Release PR is long-lived, so it is the OLDEST open PR and sorts | |
| # last -- on a busy repo it drops off page one and the job exits | |
| # green having synced nothing. | |
| # 3. The selector checks author and branch prefix, not just the label. | |
| # A label is mutable by anyone with write access, and the next step | |
| # runs `uv lock`, which executes build backends from the tree it | |
| # checked out. | |
| - id: pr_branch | |
| env: | |
| GH_TOKEN: ${{ steps.create_token.outputs.token }} | |
| GH_REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| gh pr list --state open --limit 201 \ | |
| --json headRefName,labels,author,isCrossRepository > /tmp/prs.json | |
| # Every guard below reads this file, so an empty or truncated fetch | |
| # would make all of them agree there is nothing to do -- the exact | |
| # silent-green outcome this job exists to prevent. Check the file | |
| # itself before trusting anything derived from it. | |
| if [[ ! -s /tmp/prs.json ]]; then | |
| echo "::error::gh pr list produced no output" | |
| exit 1 | |
| fi | |
| # 201 requested, so >200 is unambiguously truncation rather than a | |
| # repo that happens to have exactly the limit open. The Release PR is | |
| # the oldest open PR, so it is precisely the one that falls off. | |
| if [[ "$(jq length /tmp/prs.json)" -gt 200 ]]; then | |
| echo "::error::open PR list was truncated; raise the limit -- the Release PR may not be in this page" | |
| exit 1 | |
| fi | |
| # Matched on is_bot plus a substring, not an exact login. GitHub | |
| # renders this one identity three ways and it is easy to "fix" this | |
| # comparison into a silent no-match: | |
| # gh pr list --json author -> app/semantic-release-pusher | |
| # REST pulls/{n} user.login -> semantic-release-pusher[bot] | |
| # GraphQL Bot.login -> semantic-release-pusher | |
| # Only the first is what this step reads. The substring match holds | |
| # for all three, so gh changing its normalisation cannot silently | |
| # break the lookup. | |
| filter='map(select( | |
| .author.is_bot == true and | |
| (.author.login | contains("semantic-release-pusher")) and | |
| .isCrossRepository == false and | |
| (.headRefName | startswith("release-please--branches--")) and | |
| ((.labels // []) | any(.name == "autorelease: pending"))))' | |
| count=$(jq "$filter | length" /tmp/prs.json) | |
| if [[ "$count" -gt 1 ]]; then | |
| jq -r "$filter | .[].headRefName" /tmp/prs.json | |
| echo "::error::$count Release PRs matched; refusing to guess" | |
| exit 1 | |
| fi | |
| branch=$(jq -r "$filter | .[0].headRefName // empty" /tmp/prs.json) | |
| if [[ -z "$branch" ]]; then | |
| # An open PR on a release branch that the selector did not match | |
| # means the selector is wrong, not that there is nothing to do. | |
| # Exiting green there is the failure this whole job exists to | |
| # avoid, so make it loud and print what was actually seen. | |
| # Scoped to same-repo PRs: a fork cannot create a branch here, so | |
| # a drive-by fork PR named release-please--branches--* must not be | |
| # able to fail every release run. | |
| if jq -e 'any(.isCrossRepository == false and (.headRefName | | |
| startswith("release-please--branches--")))' /tmp/prs.json > /dev/null; then | |
| jq -r '.[] | select(.isCrossRepository == false and (.headRefName | | |
| startswith("release-please--branches--"))) | | |
| "\(.headRefName) author=\(.author.login) bot=\(.author.is_bot) labels=\([.labels[]?.name] | join(","))"' /tmp/prs.json | |
| echo "::error::a release-branch PR is open but the selector did not match it; check the autorelease label and release-please's label config, then re-run this workflow" | |
| exit 1 | |
| fi | |
| echo "no open Release PR; nothing to sync" | |
| else | |
| echo "Release PR branch: $branch" | |
| fi | |
| echo "branch=$branch" >> "$GITHUB_OUTPUT" | |
| - if: steps.pr_branch.outputs.branch != '' | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ steps.pr_branch.outputs.branch }} | |
| token: ${{ steps.create_token.outputs.token }} | |
| # Nothing here pushes over git -- the commit goes through the GraphQL | |
| # API below -- so the App token has no reason to persist in .git/config. | |
| persist-credentials: false | |
| - if: steps.pr_branch.outputs.branch != '' | |
| uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 | |
| with: | |
| python-version: "3.13" | |
| enable-cache: false | |
| # Deliberately holds no token. `uv lock` runs build backends out of the | |
| # checked-out pyproject.toml, on a branch selected partly by a mutable | |
| # label; keeping the App token out of this step means that code never | |
| # sees it. Only the next step, which touches no project code, gets it. | |
| - if: steps.pr_branch.outputs.branch != '' | |
| id: relock | |
| name: Re-lock uv.lock | |
| run: | | |
| set -euo pipefail | |
| # Unset rather than set to "0". UV_FROZEN=1 turns `uv lock` into a | |
| # no-op that exits 0 AND degrades `uv lock --check` to a warning that | |
| # also exits 0 -- so an assertion made while trusting the variable is | |
| # disabled by exactly the condition it exists to detect. Unsetting | |
| # makes both the lock and the assertion independent of the ambient | |
| # environment, which is the only way the guard means anything. | |
| unset UV_FROZEN | |
| uv lock | |
| uv lock --check | |
| if [[ -z "$(git status --porcelain uv.lock)" ]]; then | |
| echo "uv.lock already in sync" | |
| echo "changed=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| base64 -w0 < uv.lock > /tmp/uv.lock.b64 | |
| { | |
| echo "changed=true" | |
| echo "head_sha=$(git rev-parse HEAD)" | |
| } >> "$GITHUB_OUTPUT" | |
| - if: steps.relock.outputs.changed == 'true' | |
| name: Commit the lockfile, signed, through the API | |
| env: | |
| GH_TOKEN: ${{ steps.create_token.outputs.token }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| BRANCH: ${{ steps.pr_branch.outputs.branch }} | |
| HEAD_SHA: ${{ steps.relock.outputs.head_sha }} | |
| # Uses the GraphQL createCommitOnBranch mutation, not git commit/push. | |
| # Commits made via the API on an App's behalf are signed by GitHub's | |
| # app-flow key, which the required_signatures ruleset on main demands; | |
| # a plain git commit from the runner is unsigned and blocks the PR. | |
| run: | | |
| set -euo pipefail | |
| # Request built with jq --rawfile and submitted via --input. Two | |
| # failure modes this avoids: | |
| # 1. Inline `-f content=$b64` exceeds MAX_ARG_STRLEN (128KB per | |
| # arg) once uv.lock passes ~95KB (E2BIG / exit 126). | |
| # 2. `-f content=@/tmp/file` does NOT work -- gh's @filename | |
| # expansion does not apply to graphql variables; it sends the | |
| # literal path and the server rejects "Invalid Base64". | |
| # shellcheck disable=SC2016 # jq references, not shell expansions. | |
| query='mutation($repo: String!, $branch: String!, $sha: GitObjectID!, $content: Base64String!) { | |
| createCommitOnBranch(input: { | |
| branch: { repositoryNameWithOwner: $repo, branchName: $branch }, | |
| message: { headline: "chore: sync uv.lock with version bump" }, | |
| expectedHeadOid: $sha, | |
| fileChanges: { additions: [{ path: "uv.lock", contents: $content }] } | |
| }) { | |
| commit { url } | |
| } | |
| }' | |
| jq -n \ | |
| --arg query "$query" \ | |
| --arg repo "$GITHUB_REPOSITORY" \ | |
| --arg branch "$BRANCH" \ | |
| --arg sha "$HEAD_SHA" \ | |
| --rawfile content /tmp/uv.lock.b64 \ | |
| '{ | |
| query: $query, | |
| variables: { | |
| repo: $repo, | |
| branch: $branch, | |
| sha: $sha, | |
| content: ($content | rtrimstr("\n")) | |
| } | |
| }' > /tmp/graphql.json | |
| gh api graphql --input /tmp/graphql.json | |
| publish: | |
| needs: release-please | |
| if: needs.release-please.outputs.release_created == 'true' | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: pypi | |
| url: https://pypi.org/p/python-template | |
| permissions: | |
| id-token: write | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ needs.release-please.outputs.tag_name }} | |
| persist-credentials: false | |
| - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 | |
| with: | |
| python-version: "3.13" | |
| enable-cache: false | |
| - run: uv build | |
| - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 |