diff --git a/.github/workflows/nofity-slack-on-main-merge.yml b/.github/workflows/nofity-slack-on-main-merge.yml deleted file mode 100644 index 1d5e8a05..00000000 --- a/.github/workflows/nofity-slack-on-main-merge.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Notify Slack on Merge to Main - -on: - pull_request: - types: [closed] - branches: - - main - -jobs: - notify: - runs-on: ubuntu-latest - if: github.event.pull_request.merged == true - steps: - - name: Send notification to Slack - uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 - with: - webhook: ${{ secrets.SLACK_WEBHOOK_URL }} - webhook-type: incoming-webhook - payload: | - { - "text": ${{ toJSON(format('[Codeplain Client] PR <{0}|#{1}: {2}> was merged to main by `{3}`.', github.event.pull_request.html_url, github.event.pull_request.number, github.event.pull_request.title, github.actor)) }} - } diff --git a/.github/workflows/notify-slack-on-main-merge.yml b/.github/workflows/notify-slack-on-main-merge.yml new file mode 100644 index 00000000..0f6fff24 --- /dev/null +++ b/.github/workflows/notify-slack-on-main-merge.yml @@ -0,0 +1,37 @@ +name: Notify Slack on Merge to Main + +# pull_request_target runs this file from main with secrets, so merged PRs from +# forks post too. Nothing from the PR is checked out or run; the title only +# reaches the shell through an env var. +on: + pull_request_target: + types: [closed] + branches: + - main + +permissions: {} + +jobs: + notify: + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true && vars.SLACK_CHANNEL_ID != '' + steps: + # Slack reads <, > and & as markup: escape them so a PR title cannot + # mention @channel or rewrite the link. + - name: Escape the PR title for Slack + id: pr + env: + TITLE: ${{ github.event.pull_request.title }} + run: | + printf 'title=%s\n' "$(printf '%s' "$TITLE" | tr '\r\n' ' ' | sed -e 's/&/\&/g' -e 's//\>/g')" >> "$GITHUB_OUTPUT" + + - name: Send notification to Slack + uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 + with: + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + errors: true + # toJSON quotes the title, so quotes and colons in it keep the payload valid. + payload: | + channel: ${{ vars.SLACK_CHANNEL_ID }} + text: ${{ toJSON(format('[Codeplain Client] PR <{0}|#{1}: {2}> was merged to main by `{3}`.', github.event.pull_request.html_url, github.event.pull_request.number, steps.pr.outputs.title, github.actor)) }} diff --git a/.github/workflows/publish-dev-to-pypi.yml b/.github/workflows/publish-dev-to-pypi.yml index 64f58372..b954f2bc 100644 --- a/.github/workflows/publish-dev-to-pypi.yml +++ b/.github/workflows/publish-dev-to-pypi.yml @@ -110,9 +110,6 @@ jobs: # authenticate this run — no long-lived API token needed. id-token: write - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - steps: - name: Download distribution packages uses: actions/download-artifact@v8 @@ -127,21 +124,57 @@ jobs: # version should be a no-op, not a red build. skip-existing: true - - name: Notify Slack (success) - if: ${{ success() && env.SLACK_WEBHOOK_URL != '' }} - env: - VERSION: ${{ needs.build.outputs.version }} - run: | - # Spell out the --pre install: a plain "pip install codeplain" will - # NOT pick this up, which otherwise reads as a broken publish. - TEXT="*Codeplain Client*: dev build \`$VERSION\` published to PyPI :test_tube:. Normal installs are unaffected — try it with \`pip install --pre codeplain==$VERSION\`. Pre-release builds talk to \`api.test.codeplain.ai\` by default." - curl -s -X POST "$SLACK_WEBHOOK_URL" \ - -H 'Content-type: application/json' \ - --data "$(jq -n --arg t "$TEXT" '{text:$t}')" + # End-of-run notices run in their own job: a failed Slack post fails only this + # job, not the publish or its GitHub deployment, and "Re-run failed jobs" + # re-posts without publishing again. + notify: + name: Notify Slack + needs: [build, publish-dev] + if: ${{ !cancelled() && vars.SLACK_CHANNEL_ID != '' }} + runs-on: ubuntu-latest + + steps: + - name: Short SHA + run: echo "SHORT_SHA=${GITHUB_SHA:0:7}" >> "$GITHUB_ENV" + + - name: Notify Slack (build failure) + if: ${{ needs.build.result == 'failure' }} + uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 + with: + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + errors: true + payload: | + channel: ${{ vars.SLACK_CHANNEL_ID }} + text: >- + *Codeplain Client*: Building the dev build FAILED :x: (commit `${{ env.SHORT_SHA }}`). Nothing was published to PyPI. + See <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|the run log>. - name: Notify Slack (failure) - if: ${{ failure() && env.SLACK_WEBHOOK_URL != '' }} - run: | - curl -s -X POST "$SLACK_WEBHOOK_URL" \ - -H 'Content-type: application/json' \ - --data "{\"text\":\"*Codeplain Client*: Publishing dev build to PyPI FAILED :x: (commit \`${{ github.sha }}\`). See <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|the run log>.\"}" + if: ${{ needs.publish-dev.result == 'failure' }} + uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 + with: + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + errors: true + payload: | + channel: ${{ vars.SLACK_CHANNEL_ID }} + text: >- + *Codeplain Client*: Publishing dev build to PyPI FAILED :x: (commit `${{ env.SHORT_SHA }}`). + See <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|the run log>. + + # Spell out the --pre install: a plain "pip install codeplain" will + # NOT pick this up, which otherwise reads as a broken publish. + - name: Notify Slack (success) + if: ${{ needs.publish-dev.result == 'success' }} + uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 + with: + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + errors: true + payload: | + channel: ${{ vars.SLACK_CHANNEL_ID }} + text: >- + *Codeplain Client*: dev build `${{ needs.build.outputs.version }}` published to PyPI :test_tube:. + Normal installs are unaffected — try it with `pip install --pre codeplain==${{ needs.build.outputs.version }}`. + Pre-release builds talk to `api.test.codeplain.ai` by default. diff --git a/.github/workflows/publish-install-script.yml b/.github/workflows/publish-install-script.yml index bc0cf251..d291bc7f 100644 --- a/.github/workflows/publish-install-script.yml +++ b/.github/workflows/publish-install-script.yml @@ -14,9 +14,6 @@ jobs: name: Upload to Cloudflare R2 runs-on: ubuntu-latest - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -57,17 +54,43 @@ jobs: --content-type "text/plain" echo "✓ examples.ps1 uploaded to R2" - - name: Notify Slack (success) - if: ${{ success() && env.SLACK_WEBHOOK_URL != '' }} - run: | - curl -s -X POST "$SLACK_WEBHOOK_URL" \ - -H 'Content-type: application/json' \ - --data "{\"text\":\"*Codeplain Client*: Install scripts deployed to R2 :white_check_mark: (\`${GITHUB_SHA:0:7}\`, by *${{ github.actor }}*).\"}" + # End-of-run notices run in their own job: a failed Slack post fails only this + # job, and "Re-run failed jobs" re-posts without uploading again. + notify: + name: Notify Slack + needs: publish + if: ${{ !cancelled() && vars.SLACK_CHANNEL_ID != '' }} + runs-on: ubuntu-latest + permissions: {} + + steps: + - name: Short SHA + run: echo "SHORT_SHA=${GITHUB_SHA:0:7}" >> "$GITHUB_ENV" - name: Notify Slack (failure) - if: ${{ failure() && env.SLACK_WEBHOOK_URL != '' }} - run: | - curl -s -X POST "$SLACK_WEBHOOK_URL" \ - -H 'Content-type: application/json' \ - --data "{\"text\":\"*Codeplain Client*: Install script deploy to R2 FAILED :x: (\`${GITHUB_SHA:0:7}\`). Users may be getting a stale installer. See <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|the run log>.\"}" + if: ${{ needs.publish.result == 'failure' }} + uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 + with: + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + errors: true + payload: | + channel: ${{ vars.SLACK_CHANNEL_ID }} + text: >- + *Codeplain Client*: Install script deploy to R2 FAILED :x: (`${{ env.SHORT_SHA }}`). + Users may be getting a stale installer. + See <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|the run log>. + + - name: Notify Slack (success) + if: ${{ needs.publish.result == 'success' }} + uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 + with: + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + errors: true + payload: | + channel: ${{ vars.SLACK_CHANNEL_ID }} + text: >- + *Codeplain Client*: Install scripts deployed to R2 :white_check_mark: + (`${{ env.SHORT_SHA }}`, by *${{ github.actor }}*). diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 11db4a24..7afd767d 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -15,21 +15,26 @@ jobs: permissions: contents: write - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # hatch-vcs derives the version from git tags, so we need full history. fetch-depth: 0 + # Non-fatal: Slack being down must not block the release. - name: Notify Slack (start) - if: ${{ env.SLACK_WEBHOOK_URL != '' }} - run: | - curl -s -X POST "$SLACK_WEBHOOK_URL" \ - -H 'Content-type: application/json' \ - --data "{\"text\":\"*Codeplain Client*: Release \`${{ github.ref_name }}\` published by *${{ github.actor }}* — building and publishing to PyPI...\"}" + if: ${{ vars.SLACK_CHANNEL_ID != '' }} + continue-on-error: true + uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 + with: + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + errors: true + payload: | + channel: ${{ vars.SLACK_CHANNEL_ID }} + text: >- + *Codeplain Client*: Release `${{ github.ref_name }}` published by *${{ github.actor }}* + — building and publishing to PyPI... - name: Install uv # uv provisions its own Python (honoring requires-python), so no @@ -57,13 +62,6 @@ jobs: run: | gh release upload ${{ github.ref_name }} dist/* --clobber - - name: Notify Slack (build failure) - if: ${{ failure() && env.SLACK_WEBHOOK_URL != '' }} - run: | - curl -s -X POST "$SLACK_WEBHOOK_URL" \ - -H 'Content-type: application/json' \ - --data "{\"text\":\"*Codeplain Client*: Build of \`${{ github.ref_name }}\` FAILED :x:. Nothing was published to PyPI. See <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|the run log>.\"}" - publish-to-pypi: name: Publish to PyPI needs: build @@ -76,9 +74,6 @@ jobs: # authenticate this run — no long-lived API token needed. id-token: write - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - steps: - name: Download distribution packages uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -89,32 +84,79 @@ jobs: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 - - name: Notify Slack (success) - if: ${{ success() && env.SLACK_WEBHOOK_URL != '' }} - env: - # GitHub marks the release itself as a pre-release; the tag - # (v0.4.0rc1) carries the same intent for PEP 440. - IS_PRERELEASE: ${{ github.event.release.prerelease }} - TAG: ${{ github.ref_name }} - run: | - VERSION="${TAG#v}" - if [ "$IS_PRERELEASE" = "true" ]; then - # pip and uv skip pre-releases by default, so "--upgrade" would - # NOT pick this up. Tell people the command that actually works. - TEXT="*Codeplain Client*: pre-release \`$TAG\` published to PyPI :test_tube:. Normal installs are unaffected — try it with \`pip install --pre codeplain==$VERSION\`." - else - TEXT="*Codeplain Client*: \`$TAG\` published to PyPI :white_check_mark:. Install with \`pip install --upgrade codeplain\`." - fi - curl -s -X POST "$SLACK_WEBHOOK_URL" \ - -H 'Content-type: application/json' \ - --data "$(jq -n --arg t "$TEXT" '{text:$t}')" + # End-of-run notices run in their own job: a failed Slack post fails only this + # job, not the publish or its GitHub deployment, and "Re-run failed jobs" + # re-posts without publishing again. + notify: + name: Notify Slack + needs: [build, publish-to-pypi] + if: ${{ !cancelled() && vars.SLACK_CHANNEL_ID != '' }} + runs-on: ubuntu-latest + + steps: + - name: Notify Slack (build failure) + if: ${{ needs.build.result == 'failure' }} + uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 + with: + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + errors: true + payload: | + channel: ${{ vars.SLACK_CHANNEL_ID }} + text: >- + *Codeplain Client*: Build of `${{ github.ref_name }}` FAILED :x:. Nothing was published to PyPI. + See <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|the run log>. - name: Notify Slack (failure) - if: ${{ failure() && env.SLACK_WEBHOOK_URL != '' }} - run: | - curl -s -X POST "$SLACK_WEBHOOK_URL" \ - -H 'Content-type: application/json' \ - --data "{\"text\":\"*Codeplain Client*: Publishing \`${{ github.ref_name }}\` to PyPI FAILED :x:. See <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|the run log>.\"}" + if: ${{ needs.publish-to-pypi.result == 'failure' }} + uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 + with: + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + errors: true + payload: | + channel: ${{ vars.SLACK_CHANNEL_ID }} + text: >- + *Codeplain Client*: Publishing `${{ github.ref_name }}` to PyPI FAILED :x:. + See <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|the run log>. + + # The tag (v0.4.0rc1) without its "v", for the install command below. + - name: Version from tag + id: version + if: ${{ needs.publish-to-pypi.result == 'success' }} + env: + TAG: ${{ github.ref_name }} + run: echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" + + # GitHub marks the release itself as a pre-release; the tag (v0.4.0rc1) + # carries the same intent for PEP 440. pip and uv skip pre-releases by + # default, so "--upgrade" would NOT pick one up. Tell people the command + # that actually works. + - name: Notify Slack (pre-release published) + if: ${{ needs.publish-to-pypi.result == 'success' && github.event.release.prerelease }} + uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 + with: + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + errors: true + payload: | + channel: ${{ vars.SLACK_CHANNEL_ID }} + text: >- + *Codeplain Client*: pre-release `${{ github.ref_name }}` published to PyPI :test_tube:. + Normal installs are unaffected — try it with `pip install --pre codeplain==${{ steps.version.outputs.version }}`. + + - name: Notify Slack (release published) + if: ${{ needs.publish-to-pypi.result == 'success' && !github.event.release.prerelease }} + uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 + with: + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + errors: true + payload: | + channel: ${{ vars.SLACK_CHANNEL_ID }} + text: >- + *Codeplain Client*: `${{ github.ref_name }}` published to PyPI :white_check_mark:. + Install with `pip install --upgrade codeplain`. # Release pipelines to be turned on only after the rest of the infrastructure # is ready. @@ -129,9 +171,6 @@ jobs: # permissions: # contents: read # - # env: - # SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - # # steps: # - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # with: @@ -152,8 +191,14 @@ jobs: # # continue-on-error keeps the job green, so this checks the step's own # # outcome — `failure()` would never fire. # - name: Notify Slack (release not recorded) - # if: ${{ steps.linear.outcome == 'failure' && env.SLACK_WEBHOOK_URL != '' }} - # run: | - # curl -s -X POST "$SLACK_WEBHOOK_URL" \ - # -H 'Content-type: application/json' \ - # --data "{\"text\":\"*Codeplain Client*: \`${{ github.ref_name }}\` is on PyPI, but recording it in Linear FAILED :warning:. See <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|the run log>.\"}" + # if: ${{ steps.linear.outcome == 'failure' && vars.SLACK_CHANNEL_ID != '' }} + # uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 + # with: + # method: chat.postMessage + # token: ${{ secrets.SLACK_BOT_TOKEN }} + # errors: true + # payload: | + # channel: ${{ vars.SLACK_CHANNEL_ID }} + # text: >- + # *Codeplain Client*: `${{ github.ref_name }}` is on PyPI, but recording it in Linear FAILED :warning:. + # See <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|the run log>.