Slack Attachments as a Package Deployment System

The Context

As part of a current project, I’m working on an Adobe CEP extension to augment the behaviour of Adobe Illustrator to add new features.
I had already set-up our GitHub Actions CI to create builds on each merge to main to create a signed package which can be installed using the Unified Plugin Installer Agent (which gets installed alongside Adobe products by default).
We are not using the newer Unified Extensibility Platform as it is not available for Illustrator.

The Problem

Our Product Owner (PO) does not have access to our GitHub repository, so cannot access the latest builds to install on-demand. We could grant access, however this is still not the ideal delivery system (having to navigate through the GitHub UI and identify the latest build).
Previously, we were manually sharing a build via Slack as an attachment for her to download and install at the end of every day with the latest changes included. As this process of sharing the latest build took time, we were often tempted to defer sharing the build so we could squeeze in that one last PR merge into main, which batched the validation of tickets and caused delays.
I needed to think of a way to allow our PO to securely access the latest build on-demand.

The Solution

The main challenge was authentication–our builds are all available on GitHub and can be queried via the GitHub API to find the latest build URL if needed, but this requires some form of API key/ PAT to authorise the client.
We could have written a script which accepts an access token at runtime as input and uses it to download and install the latest and ask the PO to keep it in a password vault, but I didn’t want to introduce any possibility of human error in handling API keys/ PATs.
It may be obvious given the title of the journal, but the solution I came up with (for now) was to outsource the responsibility of authentication to Slack (a platform for which our PO is already authenticated) and keep the rest of the flow the same from the perspective of our PO.
notion image
On merge to main, the PO now receives a notification that a new build is ready and has on-demand access around the clock to the latest build in a secure way.
 
As a prerequisite for this method, you must create, install, and invite a Slack app in your organisation and Slack channel.
  1. Go to your Slack apps
  1. Create a new app (top left dropdown)
  1. Go to OAuth & Permissions and give chat:write and files:write
  1. Go to Install App and install it to the correct organisation
  1. Go to the channel that you want it to post to and type /invite and select Add apps to this channel
    1. Search for your app name (took some time to refresh for me) and select it
 

The Code

Given the recent spate of supply chain attacks, instead of depending on some light wrapper around the Slack API in the form of a third-party GitHub action, I decided to write my own helper action to upload Slack attachments (with the help of ChatGPT):
# .github/workflows/ci.yml name: CI on: push: branches: ["**"] tags: ["*"] pull_request: branches: ["**"] env: ... permissions: contents: write id-token: write jobs: tests: ... build_zxp_main: if: github.event_name == 'push' && github.ref == 'refs/heads/main' ... steps: ... - name: Upload ZXP artifact uses: actions/upload-artifact@v5 with: # We have a matrix running with the option to also create # a Windows distribution which we're currently not using name: ${{ matrix.platform }}.zxp path: ./dist/${{ matrix.platform }}.zxp retention-days: 14 post_to_slack: if: | ((github.event_name == 'push' && github.ref == 'refs/heads/main') || startsWith(github.ref, 'refs/tags/')) && always() needs: [...] runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 # We also post pipeline failures here ... - name: Download macOS ZXP artifact ready to upload to Slack uses: actions/download-artifact@v6 with: name: macos.zxp path: ./slack-artifacts - name: Upload macOS ZXP to Slack uses: ./.github/actions/slack-upload-file-v2 with: slack_bot_token: ${{ secrets.SLACK_BOT_TOKEN }} channel_id: ${{ secrets.SLACK_CHANNEL_ID }} file_path: ./slack-artifacts/macos.zxp initial_comment: > macOS build artifact for ${{ github.repository }} @ main (run ${{ github.run_id }}): ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# .github/actions/slack-upload-file-v2/action.yml name: "Upload a file to Slack (Upload v2)" description: "Uploads a file to Slack using files.getUploadURLExternal + files.completeUploadExternal" inputs: slack_bot_token: description: "Slack bot token (xoxb-...)" required: true channel_id: description: "Slack channel ID to share the file into" required: true file_path: description: "Path to the file to upload" required: true initial_comment: description: "Message to include with the uploaded file" required: false default: "" title: description: "Title to set on the uploaded file (defaults to basename of file_path)" required: false default: "" runs: using: "composite" steps: - name: Validate inputs shell: bash run: | set -euo pipefail if [ ! -f "${{ inputs.file_path }}" ]; then echo "File not found: ${{ inputs.file_path }}" exit 1 fi if ! command -v jq >/dev/null 2>&1; then echo "jq is required but not found. Use ubuntu-latest (jq is preinstalled) or install jq." exit 1 fi - name: Upload file to Slack (v2 flow) shell: bash env: SLACK_BOT_TOKEN: ${{ inputs.slack_bot_token }} SLACK_CHANNEL_ID: ${{ inputs.channel_id }} FILE_PATH: ${{ inputs.file_path }} INITIAL_COMMENT: ${{ inputs.initial_comment }} TITLE_INPUT: ${{ inputs.title }} run: | set -euo pipefail filename="$(basename "$FILE_PATH")" filesize="$(wc -c < "$FILE_PATH" | tr -d ' ')" title="$TITLE_INPUT" if [ -z "$title" ]; then title="$filename" fi # 1) Ask Slack for an upload URL resp="$(curl -sS -X POST https://slack.com/api/files.getUploadURLExternal \ -H "Authorization: Bearer $SLACK_BOT_TOKEN" \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "filename=$filename" \ --data-urlencode "length=$filesize")" ok="$(echo "$resp" | jq -r '.ok')" if [ "$ok" != "true" ]; then echo "Slack getUploadURLExternal failed: $resp" exit 1 fi upload_url="$(echo "$resp" | jq -r '.upload_url')" file_id="$(echo "$resp" | jq -r '.file_id')" # 2) Upload the raw bytes to the pre-signed URL curl -sS --fail -X POST "$upload_url" \ -H "Content-Type: application/octet-stream" \ --data-binary @"$FILE_PATH" >/dev/null # 3) Complete the upload and share into the channel # Slack expects "files" to be an array of {id,title}. complete_payload="$(jq -n \ --arg fid "$file_id" \ --arg ftitle "$title" \ --arg ch "$SLACK_CHANNEL_ID" \ --arg comment "$INITIAL_COMMENT" \ '{ files: [{id: $fid, title: $ftitle}], channel_id: $ch, initial_comment: $comment }')" done_resp="$(curl -sS -X POST https://slack.com/api/files.completeUploadExternal \ -H "Authorization: Bearer $SLACK_BOT_TOKEN" \ -H "Content-Type: application/json; charset=utf-8" \ -d "$complete_payload")" done_ok="$(echo "$done_resp" | jq -r '.ok')" if [ "$done_ok" != "true" ]; then echo "Slack completeUploadExternal failed: $done_resp" exit 1 fi echo "Uploaded $filename to Slack channel $SLACK_CHANNEL_ID"
 
Then add the appropriate secrets into GitHub secrets:
SLACK_BOT_TOKEN SLACK_CHANNEL_ID