Skip to content

feat(skills): add the deploy-slack-app skill for Railway and Heroku - #153

Open
mwbrooks wants to merge 17 commits into
mainfrom
mwbrooks-deploy-slack-app
Open

mwbrooks wants to merge 17 commits into
mainfrom
mwbrooks-deploy-slack-app

Conversation

@mwbrooks

@mwbrooks mwbrooks commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

Summary

This pull request gives developers a way to deploy a Socket Mode Bolt app to Railway or Heroku, from macOS or Linux, as a separate, always-on Slack app with its own app ID. The local development app is unchanged. It uses the Slack CLI's deploy hook, which runs after the CLI has installed the app, so the bot and app-level tokens are already in the script's environment and nobody has to copy a secret anywhere.

slack deploy --skip-update --team <team ID> --app deployed --org-workspace-grant all

Each provider has one self-contained script, copied into the project as .slack/deploy-railway.sh or .slack/deploy-heroku.sh, so adding a host or a PowerShell version later means adding a file rather than changing a shared one. The skill links each provider's pricing page instead of quoting prices, because plans change faster than installed skills get updated.

Testing

  • Verified end to end on macOS against Slack CLI v4.7.0, on both providers, from a freshly scaffolded bolt-js-starter-template project.
  • Ran each provider three times: first deploy, re-deploy with no code change, and re-deploy with a code change, confirming one Slack app throughout and the change live in the host's logs.
  • Confirmed the deployed app answered a hello message and /sample-command in Slack with nothing running locally, matched to the chat.postMessage and slash_commands entries in the host's logs.
  • Re-ran Railway end to end against Slack CLI v4.9.0 after the deploy scripts were hardened for agent sessions, from fresh bolt-js-starter-template and bolt-python-starter-template projects. Both deployed, re-deployed with a code change, and answered hello in Slack with nothing running locally. The JavaScript project was also re-deployed with no change.
  • Broke a Railway build on purpose with a missing npm package. slack deploy exited 1 with the build error, and the previous deployment stayed live.
  • Re-ran Heroku end to end against Slack CLI v4.9.0 after the hardening, from fresh bolt-js-starter-template and bolt-python-starter-template projects on a Heroku team. Both deployed, re-deployed with and without a code change, and answered hello in Slack with nothing running locally. The no-change re-deploys restarted the dynos, and the Python app scaled its worker without a web process type.
  • Broke a Heroku build the same way. slack deploy exited 1 with the npm error, and the previous release stayed current.
  • Tore down both providers by following the new teardown step, from fresh bolt-js-starter-template deploys against Slack CLI v4.9.0. Railway stopped the service right away and scheduled the project's removal, Heroku destroyed the app, and slack app delete --force removed the deployed app without a prompt.
  • Eval routing suite passes in full, including the scenarios whose prompts contain the word "deploy".

Manual testing

  1. With this branch's plugin installed, create an app: slack create my-app --template slack-samples/bolt-js-starter-template. The bolt-python-starter-template works too.
  2. In my-app, ask Claude Code to deploy it to Railway or Heroku, and log in to the provider CLI when it asks.
  3. Stop any local slack run, then send hello to the deployed app in Slack. It should reply.
  4. Change a log line, ask the agent to re-deploy, and confirm the line shows in the provider's logs with the same Slack app.
  5. Ask the agent to tear down the deployment. It should name the provider project or app and the deployed Slack app ID, ask once, then delete both and leave the local app in place. Delete the local app with slack app delete --app local if you're done with it.

Notes

  • slack deploy needs --app deployed and --team in a non-interactive shell, or it exits on a prompt the developer never sees.
  • Heroku's Node buildpack contributes a default web process type, so the app ran as two copies holding two Socket Mode connections until the script scaled web to zero. The worker's own logs looked healthy the entire time.
  • Some Heroku accounts cannot own a personal app at all, so HEROKU_TEAM is required for them.
  • Socket Mode only, macOS and Linux only. Request URL apps, serverless hosts such as Vercel, and a PowerShell port are follow-ups.

Requirements

  • I've read and understood the Contributing Guidelines and have done my best effort to follow them.
  • I've read and agree to the Code of Conduct.
  • I've run make test and the tests pass. Unit (21) and eval (36) pass, and typecheck is clean. make lint passes.

Gives developers a path from an app that works locally to one that keeps
running after the local process stops, using the Slack CLI deploy hook so
the skill never handles a token itself.

The hook script is provider-agnostic and sources a small per-target file,
so adding a provider is one reference file rather than a change to the
script. Railway and Heroku ship; both run the app in Socket Mode.

Adds four tool-selection eval scenarios, two that should route to the new
skill and two that pin the boundaries against slack-cli and slack-docs.
… in real runs

A real end-to-end Railway deploy surfaced a prompt Step 5 did not cover.
'slack deploy' asks which app to target, and in a non-interactive shell that
prompt is fatal: the CLI reports that the input device is not a TTY and nothing
deploys. '--team' and '--app deployed' answer it, and '--app deployed' is the one
most easily missed because the prompt only appears on a project that has never
been deployed.

A real Heroku run surfaced two more. Salesforce-managed accounts cannot own a
personal app, so 'apps:create' needs '--team'; the target script now takes
HEROKU_TEAM. And 'apps:info' reports the same 'Couldn't find that app' for an app
that does not exist and one owned by a stranger, because Heroku app names are
globally unique, so a generic name fails later at create time instead.
…cost claim

A real Heroku deploy ran two copies of the app. The Node buildpack contributes a
default 'web' process type even though the Procfile declares only 'worker', and
Heroku starts it on the first release. That second copy opened its own Socket Mode
connection, so Slack reported "num_connections": 2 and delivered events to
whichever copy it picked, and it never booted successfully either, because a Socket
Mode app binds no port and Heroku kills a web dyno that does not bind $PORT within
60 seconds. It sat in a restart loop, opening a fresh connection every cycle, and
billed as a second dyno. The worker's own logs looked healthy throughout, so
nothing about this was visible from the place a developer would look.
'target_deploy' now scales worker up and web down in one call.

The same run disproved the cost line. A team app cannot use the Eco plan and gets
Basic dynos billed per dyno, so the '$5/month' figure only holds for a personal
app. Salesforce-managed accounts cannot own a personal app at all, which makes the
team case the default rather than the exception for anyone in that position.
@mwbrooks mwbrooks added enhancement New feature or request semver:minor Changes trigger a minor version bump labels Sep 24, 2026
@changeset-bot

changeset-bot Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 44d032e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
slack Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Replace the orchestrator + four-function contract with one linear,
self-contained deploy script per provider, on disk, syntax-highlighted.
The agent no longer extracts bash out of a fenced markdown block.

- Delete references/deploy.sh and its target_* seam.
- Add references/heroku/deploy.sh and references/railway/deploy.sh
  as real executable files.
- Reduce references/heroku.md and references/railway.md to prose:
  cost, caveats, install, project prereqs, env vars, re-deploy,
  verifying.
- Collapse SKILL.md Step 4 from three sub-steps to two.

Behaviour is unchanged. The Railway path was re-verified end to end
on macOS against a fresh bolt-js-starter-template scaffold.

Windows support becomes additive: two .ps1 files beside the two .sh
files, no orchestrator to port twice.
…y-<provider>.sh

Keep the deploy script out of the project root and name it after the
provider, so it sits beside hooks.json and says which host it targets.
The hook now points at ./.slack/deploy-railway.sh or
./.slack/deploy-heroku.sh.

- Rename references/railway/deploy.sh and references/heroku/deploy.sh
  to references/deploy-railway.sh and references/deploy-heroku.sh, so
  each source file shares its installed name.
- Update SKILL.md Step 4 for the new paths, and ask before changing an
  existing deploy key that points at a different script.
- Note in each script that the CLI runs the hook from the project root,
  so its relative paths still resolve there.
The skill is installed on developer machines and can outlive a provider's
price list, so a quoted plan or trial term can go stale without us
shipping an update. Replace prices, plan names, and trial terms with
links to each provider's pricing page, and tell the agent to read or
link those pages rather than quote from memory.

- Keep the lasting reasons to pick a provider: secrets on stdin, deploy
  from the working directory vs from git, and personal vs team billing.
- Replace the Render and Fly.io comparison with the two checks that
  apply to any provider: an always-on worker process type, and how it
  is billed.
- Generalise the Heroku idle-sleep check so it no longer names a plan
  or a timeout.
… app

The deploy creates a second Slack app with its own app ID in .slack/apps.json. The old wording read as if the local development app kept running on the host.
Fetching and summarising two pricing pages spends the developer's tokens on every deploy. The skill now hands over the links and keeps only the cost facts that do not change with the price list.
… note

An agent reads this after the developer has already chosen Heroku, so the Railway aside invites a mid-deploy provider switch or a --stdin flag Heroku does not have. Step 2 already makes the comparison where the choice happens.
… agent runs

Railway: wait for the build with --ci instead of --detach, pass --workspace when set, bound every logs command with --lines, and name RAILWAY_API_TOKEN for headless auth.

Heroku: scale web=0 only when a web process type exists (the Python buildpack adds none), restart instead of an empty commit on an up-to-date push, reuse the app named by the heroku git remote, require a committed Procfile and at least one commit, and verify config without printing the tokens.

Skill: add a Prepare the Provider step, show how provider variables reach the hook, keep the language-specific get-hooks value, cover Bolt for Python and remote manifests, and drop repeated notes.
…verlap

Found in a live Railway run: the skill said to ask when an account has several workspaces without saying how to check, and a re-deploy briefly shows num_connections 2 while Railway removes the old deployment.
…st-deploy gaps

Found in live Heroku runs with Bolt for JavaScript and Bolt for Python. slack create does not run git init, so a project inside another repository passed the git checks and would have pushed the parent's code. The deploy script now stops in that case.

The docs now say to commit .slack/apps.json after the first deploy, to add a .python-version for Bolt for Python, and that num_connections 2 is expected while the first release's web dyno shuts down.
Comment thread skills/deploy-slack-app/SKILL.md Outdated
Comment thread tests/config.py
Comment on lines +24 to +31
EXPECTED_SKILLS = (
"create-slack-app",
"block-kit",
"deploy-slack-app",
"slack-api",
"slack-cli",
"slack-docs",
)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: Linter

… skill

The description no longer says "Socket Mode only, on macOS and Linux", which
told agents to skip the skill for those developers before they could see why
the case is unsupported. The Request URL and Windows stop messages now ask the
developer to update the plugin, since support may have shipped, and link
#172 and #171 to upvote. Two routing evals pin both cases.
Step 8 deletes the provider's copy and the deployed Slack app after one
confirmation, and leaves the development app in place. Verified end to end
on both providers: `railway unlink` needs `--yes`, `heroku apps:destroy`
removes the git remote itself, and `slack app delete --force` removes
`.slack/apps.json`. Railway schedules the project's removal, so the
reference explains why it stays listed. Adds a routing eval for teardown.
@mwbrooks mwbrooks self-assigned this Oct 6, 2026
@mwbrooks
mwbrooks marked this pull request as ready for review October 6, 2026 23:53
@mwbrooks
mwbrooks requested a review from a team as a code owner October 6, 2026 23:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request semver:minor Changes trigger a minor version bump

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant