v1.0

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

1
Action starts — installs deps, spins up a Flask form server on localhost.
2
Bore tunnel — exposes the local server via bore.pub (or your own server).
3
Notify — optional Slack / Discord message with the public URL.
4
User fills form — opens URL, enters data, uploads files, hits Submit.
5
Outputs set — every field becomes a step output; workflow continues.
💡
Tip
You can use bore.pub out of the box — zero setup. For production or regulated environments, self-host your own bore server.

Key features

FeatureDetails
Self-hostable tunnelUse bore.pub or run your own bore server — no external accounts required.
8 field typestext, textarea, number, boolean, select, multiselect, file, multifile.
NotificationsSlack and Discord support out of the box.
File uploadsSingle and multi-file uploads with MIME-type filtering.
Python-basedEasy to read, maintain, and extend.
Auto-redirectAfter 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:

yaml
# .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

  1. Go to Actions — open your repo on GitHub, click the Actions tab.
  2. Select your workflow — choose Interactive Deployment from the list.
  3. Run workflow — click the Run workflow button (top-right).
  4. Wait for the URL — watch the logs; a public URL will appear after ~10 seconds.
  5. Fill the form — open the URL, enter your values, hit Submit.
  6. Watch it continue — the workflow resumes and your next step can use the outputs.
Timeout
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

InputDescriptionRequiredDefault
titleTitle shown at the top of the form.Interactive Inputs
interactiveYAML definition of the fields to display (see Field Types).Yes
timeoutSeconds to wait for a submission before failing.300
bore-serverBore server hostname.Yesbore.pub
bore-portRequest a specific remote port. 0 = random.0
bore-secretAuth secret if your bore server requires one.
github-tokenToken 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 typeOutput format
text / textareaPlain string
numberNumeric string, e.g. 42
booleantrue or false
selectSelected choice string
multiselectJSON array, e.g. ["A","B"]
file / multifilePath to the upload directory
💡
Tip
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.

yaml
- 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.

yaml
- 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.

yaml
- label: replicas
  properties:
    display:    Pod Replicas
    type:       number
    minNumber:  1
    maxNumber:  20
    required:   true

boolean

A checkbox. Output is the string true or false.

yaml
- label: confirmed
  properties:
    display:      Confirm deployment?
    type:         boolean
    defaultValue: false

select

A dropdown — the user picks exactly one option.

yaml
- 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.

yaml
- 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.

yaml
- 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.

yaml
- label: migration-scripts
  properties:
    display:            Upload Migration Scripts
    type:               multifile
    acceptedFileTypes:
      - text/sql
      - application/pdf
💡
Tip
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

  1. Go to api.slack.com/apps and click Create New App → From scratch.
  2. Name the app and pick your workspace, then click Create App.
  3. Add scopes — in OAuth & Permissions → Bot Token Scopes add chat:write and chat:write.customize.
  4. Install — scroll up, click Install to Workspace, then Authorize.
  5. 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

yaml
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

  1. Channel settings — right-click your target channel → Edit Channel.
  2. Integrations — click Webhooks → New Webhook.
  3. 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

yaml
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"
💡
Tip
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

bash
# 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

bash
# 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:

ini
[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
bash
sudo systemctl enable bore
sudo systemctl start bore
sudo systemctl status bore

Use it in your workflow

yaml
with:
  bore-server: 'tunnel.yourcompany.com'
  bore-secret: ${{ secrets.BORE_SECRET }}
🔒
Security
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:

yaml
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:

yaml
  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

yaml
      - 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:

yaml
      - 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.

yaml
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.

Portal URL is not loading
  • 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.pub to rule out a self-hosted config issue.
Workflow times out before I can submit
  • 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_dispatch inputs instead.
Outputs are empty in the next step
  • Make sure your step has an id and you're referencing it correctly: ${{ steps.<your-id>.outputs.<label> }}.
  • The <label> in your output reference must match the label in 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.py step failed.
  • To pass outputs to a different job, you must declare job-level outputs:. See Advanced Patterns.
Files not uploading or not found
  • Verify the acceptedFileTypes match the file you're uploading (use wildcards like image/*).
  • 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.
Slack notification not received
  • Token must start with xoxb-. Check you copied the Bot User OAuth Token, not the user token.
  • The bot must have chat:write and chat:write.customize scopes.
  • 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.
Discord webhook not working
  • 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.pub works 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

Featureinteractive-inputsbore-interactive-inputs
Text / number / boolean inputsYesYes
Select & multi-selectYesYes
File uploadsYesYes
Slack notificationsYesYes
Discord notificationsYesYes
Self-hostable tunnelNoYes
No external signup neededNoYes
Python codebaseNoYes

Migration from interactive-inputs

Switching is a one-line change — the interactive field YAML is identical:

yaml — diff
# 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?

ScenarioRecommendation
Already on ngrok, happy with itStick with interactive-inputs — no reason to change.
Want zero external dependenciesUse bore-interactive-inputs
Regulated / compliance environmentUse bore-interactive-inputs + self-host
Open-source project, no paid servicesUse bore-interactive-inputs with bore.pub
DevOps team familiar with PythonUse bore-interactive-inputs