Replace email attachments in CI — upload a ZIP from GitHub Actions and get a shareable link
Your CI already builds the artifact. Then someone pastes it into Slack as a 40 MB attachment — or opens a throwaway Google Drive link that expires next week with no signal that anyone opened it. If the person on the ot
Your CI already builds the artifact.
Then someone pastes it into Slack as a 40 MB attachment — or opens a throwaway Google Drive link that expires next week with no signal that anyone opened it.
If the person on the other end is not living in GitHub Releases, you need a handoff link: one URL, browser-readable, optional password/expiry, and a simple “was it opened?” check.
This post walks through a practical pattern:
- Build a ZIP (or PDF) in GitHub Actions
- Upload it with one authenticated
curl - Post the share URL back on the PR (or into Slack)
I use TiniDrop for the share step because that is what we built for this exact loop — but the workflow shape is the same idea wherever you land the file: artifact in CI → link out → humans click a URL, not a binary.
When this beats GitHub Releases (and email)
Use a share link when:
- The recipient is a client, investor, or teammate who is not cloning repos
- You are shipping a finished ZIP / PDF / HTML preview, not ongoing source
- You want a lightweight “did they open it?” signal after the PR merges
- You might want a password or expiry on that one URL
Keep Releases for versioned open-source downloads. Use a link for the human handoff.
What you need
- A GitHub repo with Actions enabled
- A ZIP (or PDF) produced by your job —
dist.zip,site.zip,deck.pdf, etc. - A TiniDrop account on Solo or Pro (API keys live under Dashboard → Settings → API keys)
- An API key stored as a GitHub Actions secret — never in the repo
Generate the key once in the dashboard. Name it something like github-actions-ci. Copy it only at creation time.
Add a repository secret:
- Name:
TINIDROP_API_KEY - Value: the key (starts with
td_if you created it recently)
Minimal upload from your laptop (sanity check)
Before wiring Actions, prove the API from your machine:
curl -sS -X POST https://tinidrop.com/api/upload \
-H "x-api-key: $TINIDROP_API_KEY" \
-F "[email protected]"
A successful response includes a shareable url (and a slug). Shape:
{
"slug": "a8k2mx",
"url": "https://tinidrop.com/view/a8k2mx",
"original_name": "dist.zip",
"size_bytes": 1843200
}
Open that url in a private window. If it loads, you are ready for CI.
Note: API upload is for Solo / Pro. Free-plan uploads go through the browser (bot check). Put the key in CI secrets and rotate it if it ever leaks — same hygiene as any deploy token.
GitHub Actions: upload after build
Here is a job that builds, zips, uploads, and prints the link.
name: Share build as link
on:
workflow_dispatch:
push:
branches: [main]
jobs:
share:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: |
# replace with your real build
mkdir -p dist
echo "<h1>Preview</h1>" > dist/index.html
- name: Zip artifact
run: |
cd dist && zip -r ../dist.zip .
- name: Upload to TiniDrop
id: upload
env:
TINIDROP_API_KEY: ${{ secrets.TINIDROP_API_KEY }}
run: |
set -euo pipefail
RESP=$(curl -sS -X POST https://tinidrop.com/api/upload \
-H "x-api-key: ${TINIDROP_API_KEY}" \
-F "[email protected]")
echo "$RESP" | tee upload.json
URL=$(echo "$RESP" | jq -r '.url')
if [ -z "$URL" ] || [ "$URL" = "null" ]; then
echo "Upload failed — check API key and plan"
exit 1
fi
echo "url=$URL" >> "$GITHUB_OUTPUT"
echo "Share URL: $URL"
- name: Summary
run: |
echo "### Build ready" >> "$GITHUB_STEP_SUMMARY"
echo "[Open share link](${{ steps.upload.outputs.url }})" >> "$GITHUB_STEP_SUMMARY"
What this gives you:
- A green check on
main(or manual dispatch) - A clickable link in the Actions job summary
- No attachment bouncing through email size limits
Bonus: comment the link on the pull request
Useful when reviewers are not watching the Actions tab:
name: PR preview link
on:
pull_request:
types: [opened, synchronize]
jobs:
preview:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Build + zip
run: |
mkdir -p dist && cp -R public/* dist/ || true
# your build here…
cd dist && zip -r ../preview.zip .
- name: Upload
id: upload
env:
TINIDROP_API_KEY: ${{ secrets.TINIDROP_API_KEY }}
run: |
RESP=$(curl -sS -X POST https://tinidrop.com/api/upload \
-H "x-api-key: ${TINIDROP_API_KEY}" \
-F "[email protected]")
URL=$(echo "$RESP" | jq -r '.url')
echo "url=$URL" >> "$GITHUB_OUTPUT"
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
const url = '${{ steps.upload.outputs.url }}';
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `### Preview ready\n\nOpen the share link (view in browser):\n${url}`
});
Reviewers click once. They do not need a GitHub package download or a local unzip ritual.
PDF decks and client ZIPs
Same pattern, different artifact:
- Pitch / proposal PDF — export in CI or commit the latest PDF, upload, send the link before the call
-
Client deliverable ZIP — zip the folder after
npm run build, upload, paste into email -
HTML prototype — zip a folder that contains
index.html; the share link opens in the browser
After you send it, check whether it was opened, then follow up with a specific next step — not “just checking if you got this.”
Hardening checklist
- [ ] Secret is
TINIDROP_API_KEYin GitHub — never in YAML - [ ] Key is Solo/Pro; rotate if a fork PR could have read it (prefer environment secrets with approval for forks)
- [ ] Fail the job if
.urlis missing — do not silently “succeed” with a broken handoff - [ ] For sensitive rounds, set password and expiry on the link from the dashboard after upload
- [ ] Do not treat a share link as a substitute for Git — source stays in the repo
FAQ
Do recipients need an account?
No. They open the link in a browser.
Is this a replacement for GitHub Releases?
No. Releases win for public versioned downloads. This wins for fast, private-ish handoffs to non-Git audiences.
What file types?
PDF, ZIP, HTML/HTM, and common images. Keep the ZIP for small static previews; keep Git for real collaboration.
Where do I start without CI?
tinidrop.com → drop a file → copy the link. Wire Actions once the manual loop feels right.
I am Santosh, co-founder of TiniDrop (Würzburg). If your team still pastes build ZIPs into Slack as attachments, say what you ship in the comments — CI preview, client ZIP, or deck — and we can compare notes on the handoff.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.