Bore Interactive Inputs
A GitHub Action that creates runtime user-input portals inside your workflows — no ngrok signup, no third-party dependency. Powered by self-hostable bore tunnels and built in Python.
How it works
bore.pub (or your own server).You can use
bore.pub out of the box — zero setup. For production or regulated environments, self-host your own bore server.Key features
| Feature | Details |
|---|---|
| Self-hostable tunnel | Use bore.pub or run your own bore server — no external accounts required. |
| 8 field types | text, textarea, number, boolean, select, multiselect, file, multifile. |
| Notifications | Slack and Discord support out of the box. |
| File uploads | Single and multi-file uploads with MIME-type filtering. |
| Python-based | Easy to read, maintain, and extend. |
| Auto-redirect | After submission, user is sent back to the workflow run page. |
Quick Start
From zero to a working interactive workflow in under five minutes.
Prerequisites
- A GitHub repository with Actions enabled.
- (Optional) Slack or Discord for notifications.
1. Add the action to your repo
Create or edit .github/workflows/interactive.yml:
# .github/workflows/interactive.yml name: Interactive Deployment on: workflow_dispatch: jobs: get-inputs: runs-on: ubuntu-latest steps: - name: Get Deployment Details id: inputs uses: pratikwayal01/bore-interactive-inputs@v1.0.2 with: bore-server: 'bore.pub' title: 'Deployment Configuration' interactive: | fields: - label: environment properties: display: Select Environment type: select choices: - development - staging - production required: true - name: Use the inputs run: | echo "Environment: ${{ steps.inputs.outputs.environment }}"
2. Trigger the workflow
- Go to Actions — open your repo on GitHub, click the Actions tab.
- Select your workflow — choose Interactive Deployment from the list.
- Run workflow — click the Run workflow button (top-right).
- Wait for the URL — watch the logs; a public URL will appear after ~10 seconds.
- Fill the form — open the URL, enter your values, hit Submit.
- Watch it continue — the workflow resumes and your next step can use the outputs.
Default timeout is 300 seconds (5 min). If nobody submits in time, the workflow fails. Increase with the
timeout input if needed.Action Inputs
Every configuration knob the action exposes, organised by category.
Core
| Input | Description | Required | Default |
|---|---|---|---|
title | Title shown at the top of the form. | Interactive Inputs | |
interactive | YAML definition of the fields to display (see Field Types). | Yes | — |
timeout | Seconds to wait for a submission before failing. | 300 | |
bore-server | Bore server hostname. | Yes | bore.pub |
bore-port | Request a specific remote port. 0 = random. | 0 | |
bore-secret | Auth secret if your bore server requires one. | — | |
github-token | Token for GitHub API calls. | Yes | ${{ github.token }} |
Outputs
Every field label declared in your interactive YAML becomes an output on the step. Access them via ${{ steps.<id>.outputs.<label> }}.
| Field type | Output format |
|---|---|
| text / textarea | Plain string |
| number | Numeric string, e.g. 42 |
| boolean | true or false |
| select | Selected choice string |
| multiselect | JSON array, e.g. ["A","B"] |
| file / multifile | Path to the upload directory |
To pass outputs to a later job, declare them on the job level with
outputs: and reference ${{ steps.<id>.outputs.* }}. See the Advanced Patterns page for a full example.Field Types
Eight input types with full YAML syntax and every available property.
text
A single-line text input.
- label: username properties: display: Username type: text description: Your GitHub handle placeholder: e.g. octocat maxLength: 50 required: true
textarea
Multi-line text. Useful for release notes, configs, or long descriptions.
- label: release-notes properties: display: Release Notes type: textarea placeholder: Describe the changes... maxLength: 1000 readOnly: false
number
Numeric input with optional min/max bounds.
- label: replicas properties: display: Pod Replicas type: number minNumber: 1 maxNumber: 20 required: true
boolean
A checkbox. Output is the string true or false.
- label: confirmed properties: display: Confirm deployment? type: boolean defaultValue: false
select
A dropdown — the user picks exactly one option.
- label: region properties: display: AWS Region type: select choices: - us-east-1 - us-west-2 - eu-west-1 required: true
multiselect
Checkboxes — the user picks one or more. Output is a JSON array.
- label: features properties: display: Enable Features type: multiselect choices: - Feature A - Feature B - Feature C
file
Single file upload. Filter by MIME type with acceptedFileTypes.
- label: config-file properties: display: Upload Config type: file acceptedFileTypes: - application/json - text/yaml required: true
multifile
Multiple file upload. Same MIME filtering; output is a directory path.
- label: migration-scripts properties: display: Upload Migration Scripts type: multifile acceptedFileTypes: - text/sql - application/pdf
Use
image/* or text/* as wildcards in acceptedFileTypes to match any subtype.Notifications
Alert your team the moment an interactive form is ready — via Slack, Discord, or both.
Slack
1. Create a Slack app
- Go to api.slack.com/apps and click Create New App → From scratch.
- Name the app and pick your workspace, then click Create App.
- Add scopes — in OAuth & Permissions → Bot Token Scopes add
chat:writeandchat:write.customize. - Install — scroll up, click Install to Workspace, then Authorize.
- Copy token — grab the Bot User OAuth Token (starts with
xoxb-).
2. Store the token
In your repo: Settings → Secrets and variables → Actions → New repository secret. Name it SLACK_TOKEN, paste the value.
3. Add to workflow
with: notifier-slack-enabled: "true" notifier-slack-token: ${{ secrets.SLACK_TOKEN }} notifier-slack-channel: "#deployments" notifier-slack-bot: "Deploy Bot" # optional: reply to a specific thread notifier-slack-thread-ts: "1234567890.123456"
Discord
1. Create a webhook
- Channel settings — right-click your target channel → Edit Channel.
- Integrations — click Webhooks → New Webhook.
- Copy URL — click Copy Webhook URL.
2. Store the webhook
Same as Slack — add a secret called DISCORD_WEBHOOK in your repo settings.
3. Add to workflow
with: notifier-discord-enabled: "true" notifier-discord-webhook: ${{ secrets.DISCORD_WEBHOOK }} notifier-discord-username: "Deploy Bot" # optional: post inside a thread notifier-discord-thread-id: "1234567890123456789"
You can enable both Slack and Discord at the same time — just set both blocks in your
with: section.Self-Hosting Bore
Run your own tunnel server for full data sovereignty — ideal for regulated industries or private infrastructure.
Why self-host?
- Form data never leaves your network.
- No dependence on an external service.
- Add authentication with a shared secret.
- Pin port ranges for firewall rules.
Install bore on your server
# Option A — download pre-built binary wget https://github.com/ekzhang/bore/releases/download/v0.6.0/bore-v0.6.0-x86_64-unknown-linux-musl.tar.gz tar -xzf bore-v0.6.0-x86_64-unknown-linux-musl.tar.gz sudo mv bore /usr/local/bin/ # Option B — build from source (requires Rust) cargo install bore-cli
Run the server
# Basic bore server --bind-addr 0.0.0.0 # With authentication bore server --bind-addr 0.0.0.0 --secret your-secret-key # Restrict allowed port range bore server --bind-addr 0.0.0.0 --min-port 10000 --max-port 20000
Run as a systemd service
Create /etc/systemd/system/bore.service:
[Unit] Description=Bore Tunnel Server After=network.target [Service] Type=simple User=bore ExecStart=/usr/local/bin/bore server --bind-addr 0.0.0.0 --secret YOUR_SECRET Restart=always RestartSec=5 [Install] WantedBy=multi-user.target
sudo systemctl enable bore sudo systemctl start bore sudo systemctl status bore
Use it in your workflow
with: bore-server: 'tunnel.yourcompany.com' bore-secret: ${{ secrets.BORE_SECRET }}
Make sure your bore server's port range is allowed by your firewall, and that the
BORE_SECRET is stored as an encrypted GitHub secret — never hardcoded in workflow files.Advanced Patterns
Multi-job pipelines, conditional branches, dynamic fields, and file processing.
Passing outputs across jobs
GitHub Actions scopes outputs per-job. To use interactive values in a downstream job, declare them as job-level outputs:
jobs: get-inputs: runs-on: ubuntu-latest outputs: environment: ${{ steps.interactive.outputs.environment }} version: ${{ steps.interactive.outputs.version }} steps: - id: interactive uses: pratikwayal01/bore-interactive-inputs@v1.0.2 with: interactive: | fields: - label: environment properties: type: select choices: [dev, staging, prod] - label: version properties: type: text deploy: needs: get-inputs runs-on: ubuntu-latest steps: - run: | echo "Deploy ${{ needs.get-inputs.outputs.version }} → ${{ needs.get-inputs.outputs.environment }}"
Conditional branching
Fan out into different jobs based on user input:
deploy-dev: needs: get-inputs if: needs.get-inputs.outputs.environment == 'dev' runs-on: ubuntu-latest steps: - run: echo "Deploying to dev…" deploy-prod: needs: get-inputs if: needs.get-inputs.outputs.environment == 'prod' runs-on: ubuntu-latest steps: - run: echo "Deploying to production…"
Processing uploaded files
- name: Process uploads run: | FILE_DIR="${{ steps.interactive.outputs.migration-scripts }}" if [ -d "$FILE_DIR" ]; then for file in "$FILE_DIR"/*; do echo "Running: $file" psql -f "$file" # example: apply SQL migration done else echo "No files uploaded" fi
Dynamic choices from a previous step
Generate your select options at runtime:
- name: Fetch available tags id: tags run: | # Pull tags from your API / git / anywhere TAGS='["v1.0.0", "v1.1.0", "v2.0.0"]' echo "tags=$TAGS" >> $GITHUB_OUTPUT - name: Pick a version uses: pratikwayal01/bore-interactive-inputs@v1.0.2 with: interactive: | fields: - label: version properties: type: select choices: ${{ steps.tags.outputs.tags }}
Multi-step approval flow
Chain two interactive actions: first a quick confirm, then the full config form.
confirm: runs-on: ubuntu-latest outputs: proceed: ${{ steps.ask.outputs.proceed }} steps: - id: ask uses: pratikwayal01/bore-interactive-inputs@v1.0.2 with: title: "Step 1 — Confirm" interactive: | fields: - label: proceed properties: display: Proceed with deployment? type: boolean configure: needs: confirm if: needs.confirm.outputs.proceed == 'true' runs-on: ubuntu-latest steps: - uses: pratikwayal01/bore-interactive-inputs@v1.0.2 with: title: "Step 2 — Configuration" interactive: | fields: - label: environment properties: type: select choices: [staging, production]
Troubleshooting
Common issues and exactly how to fix them.
- Verify the bore server is running and reachable from the internet.
- Check that your firewall allows the randomly assigned port (or the one you pinned with
bore-port). - Try switching to
bore.pubto rule out a self-hosted config issue.
- Increase the timeout:
timeout: 600(10 min). - Enable Slack/Discord notifications so you're alerted as soon as the form is ready.
- For ultra-simple cases, consider plain
workflow_dispatchinputs instead.
- Make sure your step has an
idand you're referencing it correctly:${{ steps.<your-id>.outputs.<label> }}. - The
<label>in your output reference must match thelabelin your field YAML exactly (case-sensitive). - Check the action logs — if you see "Results written to …" but no "✓ All outputs set successfully", the
set_outputs.pystep failed. - To pass outputs to a different job, you must declare job-level
outputs:. See Advanced Patterns.
- Verify the
acceptedFileTypesmatch the file you're uploading (use wildcards likeimage/*). - The output is a directory path, not a file — iterate with a loop (see Advanced Patterns).
- Large files may hit the default Flask upload limit. This can be raised in a future release.
- Token must start with
xoxb-. Check you copied the Bot User OAuth Token, not the user token. - The bot must have
chat:writeandchat:write.customizescopes. - The bot must be invited to the channel — just adding the app to the workspace isn't enough.
- Double-check the channel name includes the
#prefix.
- Paste the full URL into your browser — if you get a 405 Method Not Allowed, it's valid but expects a POST.
- Check the webhook wasn't deleted in Discord.
- Test manually:
curl -X POST "$WEBHOOK_URL" -H "Content-Type: application/json" -d '{"content":"test"}' - Thread IDs must be numeric strings — don't confuse them with channel IDs.
Why Bore?
How this action compares to the alternatives — and why you might switch.
bore-interactive-inputs vs interactive-inputs
interactive-inputs (ngrok)
- Written in TypeScript
- Requires an ngrok account + authtoken
- Free tier has usage limits
- Cannot be self-hosted
- Pro plan costs $10–20/mo per user
bore-interactive-inputs (bore)
- Written in Python — easy to maintain
- Zero signup —
bore.pubworks out of the box - No usage limits on public server
- Fully self-hostable on your own infra
- Self-hosted cost: ~$5/mo VPS, no per-user fees
Feature comparison
| Feature | interactive-inputs | bore-interactive-inputs |
|---|---|---|
| Text / number / boolean inputs | Yes | Yes |
| Select & multi-select | Yes | Yes |
| File uploads | Yes | Yes |
| Slack notifications | Yes | Yes |
| Discord notifications | Yes | Yes |
| Self-hostable tunnel | No | Yes |
| No external signup needed | No | Yes |
| Python codebase | No | Yes |
Migration from interactive-inputs
Switching is a one-line change — the interactive field YAML is identical:
# Before - uses: boasiHQ/interactive-inputs@v2 with: ngrok-authtoken: ${{ secrets.NGROK_AUTHTOKEN }} # After - uses: pratikwayal01/bore-interactive-inputs@v1.0.2 with: bore-server: 'bore.pub'
That's it. All your field definitions, labels, and output references stay exactly the same.
Who should use this?
| Scenario | Recommendation |
|---|---|
| Already on ngrok, happy with it | Stick with interactive-inputs — no reason to change. |
| Want zero external dependencies | Use bore-interactive-inputs |
| Regulated / compliance environment | Use bore-interactive-inputs + self-host |
| Open-source project, no paid services | Use bore-interactive-inputs with bore.pub |
| DevOps team familiar with Python | Use bore-interactive-inputs |