publish: getsentry/junior@0.78.0 #11317
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: Publish | |
| on: | |
| issues: | |
| types: [labeled] | |
| concurrency: | |
| # Use the issue title (e.g. "publish: getsentry/foo@1.2.3") so duplicate | |
| # issues for the same repo@version share a concurrency group. | |
| group: ${{ github.event.issue.title }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| issues: write | |
| packages: write | |
| jobs: | |
| # When accepted is added to a publish issue: | |
| # - Add ci-pending (and remove ci-failed if retrying) | |
| # - Enable the poller via CI_POLLER_HAS_PENDING=true | |
| # - Comment on the issue | |
| # - Trigger the poller immediately so we don't wait for the next cron tick | |
| # The publish job below requires ci-ready, so it will not fire until the | |
| # poller flips ci-pending → ci-ready (which also prevents publishing without | |
| # CI verification in the auto-approve race). | |
| waiting-for-ci: | |
| runs-on: ubuntu-latest | |
| name: Waiting for CI | |
| environment: production | |
| if: >- | |
| github.event.label.name == 'accepted' | |
| && github.event.issue.state == 'open' | |
| && startsWith(github.event.issue.title, 'publish: ') | |
| steps: | |
| - name: Get auth token | |
| id: token | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }} | |
| private-key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }} | |
| # Reset to a clean ci-pending state: | |
| # - Remove ci-failed (retries after CI was fixed) | |
| # - Remove ci-ready (retries after a publish failure — if we leave | |
| # ci-ready, the poller's later --add-label won't generate a | |
| # labeled event and publish.yml would never fire) | |
| # - Add ci-pending | |
| # Label ops are idempotent: --add/--remove don't fail if already | |
| # added/removed. | |
| - name: Mark ci-pending | |
| env: | |
| GH_TOKEN: ${{ steps.token.outputs.token }} | |
| run: | | |
| gh issue edit "${{ github.event.issue.number }}" \ | |
| -R "$GITHUB_REPOSITORY" \ | |
| --remove-label "ci-failed" \ | |
| --remove-label "ci-ready" \ | |
| --add-label "ci-pending" | |
| - name: Comment on issue | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| if [[ "${{ contains(github.event.issue.labels.*.name, 'ci-failed') }}" == "true" ]]; then | |
| body="Retrying — CI was previously failed. Checking CI status now." | |
| else | |
| body="Approved. Checking CI status on the release branch. Publishing will start automatically when CI passes." | |
| fi | |
| gh issue comment "${{ github.event.issue.number }}" \ | |
| -R "$GITHUB_REPOSITORY" \ | |
| --body "$body" | |
| # Best-effort: enable the cron poller. Uses a dedicated app since | |
| # sentry-internal-app lacks actions_variables:write. | |
| - name: Get poller app token | |
| id: poller-token | |
| continue-on-error: true | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| client-id: ${{ vars.CI_POLLER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CI_POLLER_APP_PRIVATE_KEY }} | |
| - name: Enable cron poller | |
| if: steps.poller-token.outcome == 'success' | |
| env: | |
| GH_TOKEN: ${{ steps.poller-token.outputs.token }} | |
| run: | | |
| gh variable set CI_POLLER_HAS_PENDING -R "$GITHUB_REPOSITORY" -b "true" | |
| # Trigger the CI poller immediately instead of waiting for the next cron tick. | |
| # Uses the app token — GITHUB_TOKEN workflow_dispatch events are suppressed. | |
| - name: Trigger CI poller | |
| env: | |
| GH_TOKEN: ${{ steps.token.outputs.token }} | |
| run: | | |
| gh workflow run ci-poller.yml -R "$GITHUB_REPOSITORY" | |
| publish: | |
| runs-on: ubuntu-latest | |
| environment: production | |
| name: Publish a new version | |
| # Publish when ci-ready is present (added by the poller after CI passes). | |
| # Fires ONLY on ci-ready label events — not accepted — to avoid racing | |
| # with waiting-for-ci on the same event. The poller always adds ci-ready | |
| # after checking CI (even if ci-ready was already present, waiting-for-ci | |
| # removes it first so a fresh labeled event fires), so this gate is | |
| # guaranteed to trigger on the happy path. | |
| if: >- | |
| github.event.issue.state == 'open' | |
| && github.event.label.name == 'ci-ready' | |
| && contains(github.event.issue.labels.*.name, 'accepted') | |
| && contains(github.event.issue.labels.*.name, 'ci-ready') | |
| && !contains(github.event.issue.labels.*.name, 'ci-pending') | |
| && !contains(github.event.issue.labels.*.name, 'ci-failed') | |
| timeout-minutes: 90 | |
| env: | |
| SENTRY_DSN: "https://303a687befb64dc2b40ce4c96de507c5@o1.ingest.sentry.io/6183838" | |
| steps: | |
| - name: Get repo contents | |
| uses: actions/checkout@v6 | |
| with: | |
| path: .__publish__ | |
| - name: Setup Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| cache: yarn | |
| cache-dependency-path: .__publish__/yarn.lock | |
| - name: Install yarn dependencies | |
| run: yarn install --cwd ".__publish__" | |
| - name: Parse and set inputs | |
| id: inputs | |
| run: node .__publish__/src/publish/inputs.js | |
| - name: Inform start | |
| if: steps.inputs.outcome == 'success' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: node .__publish__/src/publish/post-workflow-details.js | |
| # Setting the target repo branch will cause the craft config (.craft.yml) to be taken from the checked out branch | |
| # By default, we check out the default branch of the repo. | |
| # If you need to maintain diverging craft configs on different branches, add your repo and the merge target branch | |
| # (i.e. the branch craft will merge the release branch into) into the if condition below. | |
| - name: Set target repo checkout branch | |
| # Note: Branches registered here MUST BE protected in the target repo! | |
| if: | | |
| fromJSON(steps.inputs.outputs.result).repo == 'sentry-migr8' && fromJSON(steps.inputs.outputs.result).merge_target == 'tmp-merge-target' || | |
| fromJSON(steps.inputs.outputs.result).repo == 'sentry-javascript' && fromJSON(steps.inputs.outputs.result).merge_target == 'v9' || | |
| fromJSON(steps.inputs.outputs.result).repo == 'sentry-javascript' && fromJSON(steps.inputs.outputs.result).merge_target == 'v8' || | |
| fromJSON(steps.inputs.outputs.result).repo == 'sentry-javascript' && fromJSON(steps.inputs.outputs.result).merge_target == 'v7' || | |
| fromJSON(steps.inputs.outputs.result).repo == 'sentry-javascript' && fromJSON(steps.inputs.outputs.result).merge_target == 'master' || | |
| fromJSON(steps.inputs.outputs.result).repo == 'sentry-python' && fromJSON(steps.inputs.outputs.result).merge_target == 'alpha' || | |
| fromJSON(steps.inputs.outputs.result).repo == 'sentry-wizard' && fromJSON(steps.inputs.outputs.result).merge_target == '1.x' || | |
| false | |
| id: target-repo-branch | |
| env: | |
| MERGE_TARGET: ${{ fromJSON(steps.inputs.outputs.result).merge_target }} | |
| REPO: ${{ fromJSON(steps.inputs.outputs.result).repo }} | |
| run: | | |
| echo "taking craft config from branch \"$MERGE_TARGET\" in \"$REPO\"" | |
| echo "target_repo_branch=$MERGE_TARGET" >> "$GITHUB_OUTPUT" | |
| - name: Get Release Bot auth token | |
| id: token | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| client-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} | |
| private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} | |
| owner: getsentry # create token that have access to all repos | |
| - uses: actions/checkout@v6 | |
| name: Check out target repo | |
| if: ${{ steps.inputs.outputs.result }} | |
| with: | |
| path: __repo__ | |
| ref: ${{ steps.target-repo-branch.outputs.target_repo_branch || ''}} | |
| repository: getsentry/${{ fromJSON(steps.inputs.outputs.result).repo }} | |
| token: ${{ steps.token.outputs.token }} | |
| fetch-depth: 0 | |
| - name: Set targets | |
| shell: bash | |
| if: fromJSON(steps.inputs.outputs.result).targets | |
| env: | |
| CRAFT_PUBLISH_REPO: ${{ fromJSON(steps.inputs.outputs.result).repo }} | |
| CRAFT_PUBLISH_PATH: ${{ fromJSON(steps.inputs.outputs.result).path }} | |
| CRAFT_PUBLISH_VERSION: ${{ fromJSON(steps.inputs.outputs.result).version }} | |
| CRAFT_PUBLISH_TARGETS_JSON: ${{ toJSON(fromJSON(steps.inputs.outputs.result).targets) }} | |
| run: | | |
| # Render the "already published" JSON. | |
| payload="$(jq -n --argjson source "$CRAFT_PUBLISH_TARGETS_JSON" '[{($source[]): true }] | add | {"published": (. // {}) }')" | |
| # Write the state file to the safe location Craft reads from | |
| # (getsentry/craft#797, released in 2.26.0). Craft sees cwd | |
| # `/github/workspace/__repo__/<path>` inside the container; | |
| # the state dir is pinned to `$GITHUB_WORKSPACE/.craft-state` | |
| # (mapped to `/github/workspace/.craft-state` in the | |
| # container) via XDG_STATE_HOME. That path is outside | |
| # __repo__/ so repo contents cannot pre-populate it. | |
| # | |
| # We hash the CONTAINER cwd (what Craft's Node process sees), | |
| # not the runner host path. CRAFT_PUBLISH_PATH is either "." | |
| # (root) or "./subdir/..." (monorepo); after bash `cd | |
| # __repo__/<path>` and Node's process.cwd() canonicalisation, | |
| # the cwd is `/github/workspace/__repo__` (root) or | |
| # `/github/workspace/__repo__/subdir/...` (monorepo). | |
| case "$CRAFT_PUBLISH_PATH" in | |
| .|./) container_cwd="/github/workspace/__repo__" ;; | |
| ./*) container_cwd="/github/workspace/__repo__/${CRAFT_PUBLISH_PATH#./}" ;; | |
| *) container_cwd="/github/workspace/__repo__/${CRAFT_PUBLISH_PATH}" ;; | |
| esac | |
| # Strip any trailing slash to match Node's canonicalisation. | |
| container_cwd="${container_cwd%/}" | |
| cwd_hash="$(printf %s "$container_cwd" | sha1sum | cut -c1-12)" | |
| sanitise() { printf %s "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9._-]\+/_/g; s/^_\+//; s/_\+$//'; } | |
| owner_sanitised="$(sanitise getsentry)" | |
| repo_sanitised="$(sanitise "$CRAFT_PUBLISH_REPO")" | |
| version_sanitised="$(sanitise "$CRAFT_PUBLISH_VERSION")" | |
| state_dir="$GITHUB_WORKSPACE/.craft-state/craft" | |
| state_file="$state_dir/publish-state-${owner_sanitised}-${repo_sanitised}-${cwd_hash}-${version_sanitised}.json" | |
| mkdir -p "$state_dir" | |
| printf %s "$payload" > "$state_file" | |
| echo "Wrote state file: $state_file" | |
| - uses: docker://getsentry/craft:latest | |
| name: Publish using Craft | |
| with: | |
| entrypoint: /bin/bash | |
| args: >- | |
| -e | |
| -c " | |
| export HOME=/root && | |
| cd __repo__/${{ fromJSON(steps.inputs.outputs.result).path }} && | |
| exec craft publish ${{ fromJSON(steps.inputs.outputs.result).version }} | |
| " | |
| env: | |
| # Pin Craft's publish-state directory to a path outside | |
| # __repo__/ so repo contents cannot pre-populate it. See the | |
| # `Set targets` step above. | |
| XDG_STATE_HOME: /github/workspace/.craft-state | |
| CRAFT_MERGE_TARGET: ${{ fromJSON(steps.inputs.outputs.result).merge_target }} | |
| CRAFT_LOG_LEVEL: ${{ vars.CRAFT_LOG_LEVEL || 'Info' }} | |
| CRAFT_DRY_RUN: ${{ fromJSON(steps.inputs.outputs.result).dry_run }} | |
| GIT_COMMITTER_NAME: sentry-release-bot[bot] | |
| GIT_AUTHOR_NAME: sentry-release-bot[bot] | |
| EMAIL: 180476844+sentry-release-bot[bot]@users.noreply.github.com | |
| GITHUB_TOKEN: ${{ steps.token.outputs.token }} | |
| # We need to use separate tokens for GHCR.IO and GitHub API access | |
| # Because we can only access ghcr.io with GITHUB_TOKEN but that token | |
| # cannot do other cross-repo operations like our Release Bot App | |
| # Thanks GitHub | |
| DOCKER_GHCR_IO_USERNAME: x-access-token # for ghcr.io auth | |
| DOCKER_GHCR_IO_PASSWORD: ${{ secrets.GITHUB_TOKEN }} # for ghcr.io auth | |
| COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} | |
| CRAFT_GCS_TARGET_CREDS_JSON: ${{ secrets.CRAFT_GCS_TARGET_CREDS_JSON }} | |
| CRAFT_GCS_STORE_CREDS_JSON: ${{ secrets.CRAFT_GCS_STORE_CREDS_JSON }} | |
| CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} | |
| DOCKER_USERNAME: sentrybuilder | |
| DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} | |
| HEX_API_KEY: ${{ secrets.HEX_API_KEY }} | |
| TWINE_USERNAME: __token__ | |
| TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} | |
| TWINE_VERBOSE: "1" | |
| NPM_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| GEM_HOST_API_KEY: ${{ secrets.GEM_HOST_API_KEY }} | |
| AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| NUGET_API_TOKEN: ${{ secrets.NUGET_API_TOKEN }} | |
| POWERSHELL_API_KEY: ${{ secrets.POWERSHELL_API_KEY }} | |
| GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} | |
| GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} | |
| OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} | |
| OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} | |
| PUBDEV_ACCESS_TOKEN: ${{ secrets.PUBDEV_ACCESS_TOKEN }} | |
| PUBDEV_REFRESH_TOKEN: ${{ secrets.PUBDEV_REFRESH_TOKEN }} | |
| - name: Update completed targets and remove label | |
| if: ${{ cancelled() || failure() }} | |
| env: | |
| PUBLISH_ARGS: ${{ steps.inputs.outputs.result }} | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: node .__publish__/src/publish/update-issue.js | |
| - name: Inform about cancellation | |
| if: ${{ cancelled() }} | |
| env: | |
| PUBLISH_ARGS: ${{ steps.inputs.outputs.result }} | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: node .__publish__/src/publish/post-result.js cancelled | |
| - name: Inform about failure | |
| if: ${{ failure() }} | |
| env: | |
| PUBLISH_ARGS: ${{ steps.inputs.outputs.result }} | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: node .__publish__/src/publish/post-result.js failure | |
| - name: Close on success | |
| if: ${{ success() }} | |
| env: | |
| PUBLISH_ARGS: ${{ steps.inputs.outputs.result }} | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: node .__publish__/src/publish/post-result.js success |