Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 55 additions & 18 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
name: Publish

# Triggered when a version tag (`v*`) is pushed — typically by `pnpm run release`,
# which uses release-it to bump the version, commit, tag, push, and create a
# GitHub Release with auto-generated notes. The npm publish itself happens here
# so a) the publish artifact is built in a clean CI environment, and b) the git
# tag and the npm version are guaranteed to match.
#
# Tag-driven releases are branch-agnostic: the version decides the channel — a
# prerelease version (any `-` suffix) publishes under the `beta` dist-tag.

on:
push:
branches: [main]
tags:
- 'v*'
workflow_dispatch:

jobs:
Expand All @@ -12,37 +22,64 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- uses: actions/setup-node@v4
- uses: actions/setup-node@v7
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'

- uses: pnpm/action-setup@v4
# Publish exactly once per version: skip when this version is already on
# the registry (re-pushed tags and re-runs are safe no-ops), and derive
# the dist-tag from the version so a prerelease can never land on `latest`.
- id: version
run: |
NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
if npm view "$NAME@$VERSION" version >/dev/null 2>&1; then
echo "$NAME@$VERSION already on the registry — nothing to publish"
echo "publish=false" >> "$GITHUB_OUTPUT"
else
if [[ "$VERSION" == *-* ]]; then TAG=beta; else TAG=latest; fi
echo "publish=true" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
fi

- if: steps.version.outputs.publish == 'true'
uses: pnpm/action-setup@v6
with:
version: 10.12.4

- run: pnpm install
- if: steps.version.outputs.publish == 'true'
run: pnpm install --frozen-lockfile

- if: steps.version.outputs.publish == 'true'
run: pnpm lint

- run: pnpm run build
- if: steps.version.outputs.publish == 'true'
run: pnpm test --run

- run: pnpm publish --access public --no-git-checks --provenance
- if: steps.version.outputs.publish == 'true'
run: pnpm run build

- if: steps.version.outputs.publish == 'true'
run: pnpm publish --access public --no-git-checks --provenance --tag "${{ steps.version.outputs.tag }}"
env:
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}

- if: success()
uses: slackapi/slack-github-action@v1.23.0
- if: steps.version.outputs.publish == 'true'
uses: slackapi/slack-github-action@v4
with:
payload: '{"text":"✅ n8n-nodes-shipstatic published successfully"}'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "✅ n8n-nodes-shipstatic ${{ steps.version.outputs.version }} published (${{ steps.version.outputs.tag }})"

- if: failure()
uses: slackapi/slack-github-action@v1.23.0
uses: slackapi/slack-github-action@v4
with:
payload: '{"text":"🔴 n8n-nodes-shipstatic publish failed"}'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "🔴 n8n-nodes-shipstatic publish failed"
23 changes: 23 additions & 0 deletions .release-it.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://unpkg.com/release-it/schema/release-it.json",
"git": {
"tagName": "v${version}",
"tagAnnotation": "v${version}",
"commitMessage": "v${version}",
"requireBranch": "main",
"requireCleanWorkingDir": true,
"push": true
},
"github": {
"release": true,
"releaseName": "v${version}",
"autoGenerate": true
},
"npm": {
"publish": false
},
"hooks": {
"before:init": ["pnpm lint", "pnpm test --run"],
"after:bump": "pnpm run build"
}
}
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ For permanent deployments and full control over your sites and domains, add a fr

| Operation | Description |
| ---------- | -------------------------------------------------------------------------- |
| **Deploy** | Publish files and get a live URL instantly |
| **Deploy** | Publish files and get a live URL instantly (optionally password-protected) |
| **Get** | Get deployment details including URL, status, file count, size, and labels |
| **List** | List all deployments with their URLs, status, and labels |
| **Remove** | Permanently remove a deployment and all its files |
Expand All @@ -63,7 +63,7 @@ For permanent deployments and full control over your sites and domains, add a fr
| Operation | Description |
| ------------ | ------------------------------------------------------------------------------- |
| **Get** | Get domain details including linked deployment, verification status, and labels |
| **List** | List all domains with their linked deployments and verification status |
| **List** | List all domains with their linked deployment and verification status |
| **Records** | Get the DNS records you need to configure at your DNS provider |
| **Remove** | Permanently disconnect and remove a custom domain |
| **Set** | Connect a custom domain to your site, switch deployments, or update labels |
Expand Down
13 changes: 12 additions & 1 deletion nodes/Shipstatic/Shipstatic.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ async function handleDeploy(
};
const labels = parseLabels(options.labels as string);
if (labels) formData.labels = JSON.stringify(labels);
const password = (options.password as string | undefined)?.trim();
if (password) formData.password = password;

// 4. Resolve auth — API key for permanent deploys, agent token for public/temporary
let authorization: string;
Expand Down Expand Up @@ -255,7 +257,7 @@ export class Shipstatic implements INodeType {
{
name: 'List',
value: 'list',
description: 'List all domains with their linked deployments and verification status',
description: 'List all domains with their linked deployment and verification status',
action: 'List all domains',
},
{
Expand Down Expand Up @@ -448,6 +450,15 @@ export class Shipstatic implements INodeType {
placeholder: 'production, v2',
description: 'Comma-separated labels',
},
{
displayName: 'Password',
name: 'password',
type: 'string',
typeOptions: { password: true },
default: '',
description:
'Optional password to protect the deployment. Visitors must enter it to view the site. Length 6 to 128 characters.',
},
],
},

Expand Down
30 changes: 30 additions & 0 deletions tests/Shipstatic.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,36 @@ describe('deploy', () => {
expect(fd.labels).toBe('["prod","v2"]');
});

it('forwards a non-empty password to formData', async () => {
const ctx = createContext({
resource: 'deployment',
operation: 'deploy',
binaryData: true,
binaryPropertyName: 'data',
options: { password: 'hunter2!' },
});

await node.execute.call(ctx);

const fd = getFormData(ctx);
expect(fd.password).toBe('hunter2!');
});

it('omits password when blank or whitespace', async () => {
const ctx = createContext({
resource: 'deployment',
operation: 'deploy',
binaryData: true,
binaryPropertyName: 'data',
options: { password: ' ' },
});

await node.execute.call(ctx);

const fd = getFormData(ctx);
expect(fd.password).toBeUndefined();
});

it('collects multiple items into one deployment', async () => {
const ctx = createContext({
resource: 'deployment',
Expand Down