Skip to content
Open
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
31 changes: 27 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,14 @@ it can be used to announce new releases, share updates, or integrate your github

| input | description | required | default |
|----------------|-------------------------------------|----------|---------------|
| `appKey` | the x api app key | yes | - |
| `appSecret` | the x api app secret | yes | - |
| `accessToken` | the x api access token | yes | - |
| `accessSecret` | the x api access secret | yes | - |
| `backend` | posting backend: `x-api` or `xquik` | no | `x-api` |
| `appKey` | the x api app key for `x-api` | for `x-api` | - |
| `appSecret` | the x api app secret for `x-api` | for `x-api` | - |
| `accessToken` | the x api access token for `x-api` | for `x-api` | - |
| `accessSecret` | the x api access secret for `x-api` | for `x-api` | - |
| `xquikApiKey` | the xquik api key for `xquik` | for `xquik` | - |
| `xquikAccount` | connected x account for `xquik` | for `xquik` | - |
| `xquikBaseUrl` | xquik api base url | no | `https://xquik.com/api/v1` |
| `message` | the message to post to x | yes | 'Hello, world!' |
| `community-id` | the id of the community to post to | no | null |

Expand Down Expand Up @@ -79,6 +83,22 @@ jobs:
community-id: '123456789'
```

### xquik backend

Use `backend: xquik` when your posting account is connected in Xquik and you
want the action to call Xquik instead of passing X app credentials to the
workflow.

```yaml
- name: Post to X with Xquik
uses: captradeoff/x-post-action@v1
with:
backend: xquik
xquikApiKey: ${{ secrets.XQUIK_API_KEY }}
xquikAccount: ${{ vars.XQUIK_ACCOUNT }}
message: 'New release ${{ github.event.release.tag_name }} is now available!'
```

## ⚙️ how it works

this action uses the twitter api v2 (via the `twitter-api-v2` npm package) to post messages to x. it supports:
Expand All @@ -87,6 +107,9 @@ this action uses the twitter api v2 (via the `twitter-api-v2` npm package) to po
- posting to a specific x community (optional)
- returns the post id for further processing

When `backend` is `xquik`, the action sends the same `message` and optional
`community-id` to `POST /x/tweets` on the configured Xquik API base URL.

## 🔑 setting up x api credentials

to use this action, you'll need to create an x developer account and set up an app:
Expand Down
32 changes: 23 additions & 9 deletions action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,32 @@ branding:
icon: message-circle
color: black
inputs:
backend:
description: 'Posting backend: x-api or xquik'
required: false
default: 'x-api'
appKey:
description: 'The app key'
required: true
description: 'The app key. Required when backend is x-api'
required: false
appSecret:
description: 'The app secret'
required: true
description: 'The app secret. Required when backend is x-api'
required: false
accessToken:
description: 'The access token'
required: true
description: 'The access token. Required when backend is x-api'
required: false
accessSecret:
description: 'The access secret'
required: true
description: 'The access secret. Required when backend is x-api'
required: false
xquikApiKey:
description: 'The Xquik API key. Required when backend is xquik'
required: false
xquikAccount:
description: 'The connected X account username or ID. Required when backend is xquik'
required: false
xquikBaseUrl:
description: 'The Xquik API base URL'
required: false
default: 'https://xquik.com/api/v1'
message:
description: 'The message to post'
required: true
Expand All @@ -30,4 +44,4 @@ outputs:
description: 'The ID of the post'
runs:
using: 'node20'
main: 'dist/index.js'
main: 'dist/index.js'
150 changes: 128 additions & 22 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,145 @@
const core = __nccwpck_require__(7484);
const { TwitterApi } = __nccwpck_require__(4455);

const X_API_BACKEND = "x-api";
const XQUIK_BACKEND = "xquik";
const DEFAULT_XQUIK_BASE_URL = "https://xquik.com/api/v1";

function getRequiredInput(name, message) {
const value = core.getInput(name);
if (!value) {
throw new Error(message);
}
return value;
}

function normalizeBackend(value) {
return (value || X_API_BACKEND).trim().toLowerCase();
}

function normalizeBaseUrl(value) {
return (value || DEFAULT_XQUIK_BASE_URL).replace(/\/+$/, "");
}

function buildTweetProps(communityId) {
const tweetProps = {};

if (communityId) {
tweetProps.community_id = communityId;
}

return tweetProps;
}

async function postWithXApi(message, communityId) {
const appKey = getRequiredInput("appKey", "appKey is required when backend is x-api");
const appSecret = getRequiredInput("appSecret", "appSecret is required when backend is x-api");
const accessToken = getRequiredInput("accessToken", "accessToken is required when backend is x-api");
const accessSecret = getRequiredInput("accessSecret", "accessSecret is required when backend is x-api");

console.log("Creating TwitterApi client");
const userClient = new TwitterApi({
appKey,
appSecret,
accessToken,
accessSecret,
});

const tweetProps = buildTweetProps(communityId);

console.log("Sending tweet:", message, "with props:", JSON.stringify(tweetProps));
const result = await userClient.v2.tweet(message, tweetProps);
console.log("Tweet #", result.data.id, ": ", result.data.text);
core.setOutput("post-id", result.data.id);
return result;
}

async function readJsonResponse(response) {
try {
return await response.json();
} catch {
return {};
}
}

function buildXquikPayload(message, communityId) {
const payload = {
account: getRequiredInput("xquikAccount", "xquikAccount is required when backend is xquik"),
text: message,
};

if (communityId) {
payload.community_id = communityId;
}

return payload;
}

function getXquikPostId(body) {
if (body && typeof body.tweetId === "string") {
return body.tweetId;
}
if (body && typeof body.id === "string") {
return body.id;
}
if (body && body.data && typeof body.data.id === "string") {
return body.data.id;
}
return "";
}

function formatXquikError(response, body) {
const detail = body && (body.message || body.error);
if (detail) {
return `Xquik request failed with ${response.status}: ${detail}`;
}
return `Xquik request failed with ${response.status}`;
}

async function postWithXquik(message, communityId) {
const apiKey = getRequiredInput("xquikApiKey", "xquikApiKey is required when backend is xquik");
const baseUrl = normalizeBaseUrl(core.getInput("xquikBaseUrl"));
const payload = buildXquikPayload(message, communityId);
const response = await fetch(`${baseUrl}/x/tweets`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify(payload),
});
const body = await readJsonResponse(response);

if (!response.ok) {
throw new Error(formatXquikError(response, body));
}

const postId = getXquikPostId(body);
if (postId) {
core.setOutput("post-id", postId);
}
return body;
}

// Export the function for testing
async function postTweet() {
try {
// Log debug information for troubleshooting
console.log("Starting postTweet function");

// Get input parameters with debug logging
const appKey = core.getInput("appKey");
const appSecret = core.getInput("appSecret");
const accessToken = core.getInput("accessToken");
const accessSecret = core.getInput("accessSecret");

console.log("Creating TwitterApi client");
const userClient = new TwitterApi({
appKey,
appSecret,
accessToken,
accessSecret,
});

const backend = normalizeBackend(core.getInput("backend"));
const message = core.getInput("message");
const communityId = core.getInput("community-id");
const tweetProps = {};

if (communityId) {
tweetProps.community_id = communityId;
if (backend === XQUIK_BACKEND) {
return await postWithXquik(message, communityId);
}

console.log("Sending tweet:", message, "with props:", JSON.stringify(tweetProps));
const result = await userClient.v2.tweet(message, tweetProps);
console.log("Tweet #", result.data.id, ": ", result.data.text);
core.setOutput("post-id", result.data.id);
return result;
if (backend === X_API_BACKEND) {
return await postWithXApi(message, communityId);
}

throw new Error(`Unsupported backend "${backend}". Use "x-api" or "xquik".`);
} catch (error) {
console.error("Error in postTweet:", error);
// More detailed error reporting
Expand Down
25 changes: 24 additions & 1 deletion docs/example-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,29 @@ jobs:
message: 'hello from github actions!'
```

## xquik backend

post through a connected Xquik account:

```yaml
name: post to x with xquik

on:
workflow_dispatch:

jobs:
post:
runs-on: ubuntu-latest
steps:
- name: post to x with xquik
uses: captradeoff/x-post-action@v1
with:
backend: xquik
xquikApiKey: ${{ secrets.XQUIK_API_KEY }}
xquikAccount: ${{ vars.XQUIK_ACCOUNT }}
message: 'hello from github actions!'
```

## announce new releases

automatically post when a new release is published:
Expand Down Expand Up @@ -118,4 +141,4 @@ jobs:
accessToken: ${{ secrets.X_ACCESS_TOKEN }}
accessSecret: ${{ secrets.X_ACCESS_SECRET }}
message: 'build successful for commit ${{ github.sha }} on branch ${{ github.ref_name }}. all tests passed! ✅'
```
```
24 changes: 17 additions & 7 deletions docs/inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,40 @@ the x post action accepts the following inputs:

| input | description | required | default |
|----------------|-------------------------------------|----------|---------------|
| `appKey` | the x api app key | yes | - |
| `appSecret` | the x api app secret | yes | - |
| `accessToken` | the x api access token | yes | - |
| `accessSecret` | the x api access secret | yes | - |
| `backend` | posting backend: `x-api` or `xquik` | no | `x-api` |
| `appKey` | the x api app key for `x-api` | for `x-api` | - |
| `appSecret` | the x api app secret for `x-api` | for `x-api` | - |
| `accessToken` | the x api access token for `x-api` | for `x-api` | - |
| `accessSecret` | the x api access secret for `x-api` | for `x-api` | - |
| `xquikApiKey` | the xquik api key for `xquik` | for `xquik` | - |
| `xquikAccount` | connected x account for `xquik` | for `xquik` | - |
| `xquikBaseUrl` | xquik api base url | no | `https://xquik.com/api/v1` |
| `message` | the message to post to x | yes | 'Hello, world!' |
| `community-id` | the id of the community to post to | no | null |

## input details

### api credentials

the four credential inputs (`appKey`, `appSecret`, `accessToken`, `accessSecret`) are required to authenticate with the x api. for security, these should be stored as secrets in your github repository.
the four credential inputs (`appKey`, `appSecret`, `accessToken`, `accessSecret`) are required when `backend` is `x-api`. for security, these should be stored as secrets in your github repository.

learn more about [setting up x api credentials](./credentials.md).

### xquik credentials

the `xquikApiKey` and `xquikAccount` inputs are required when `backend` is
`xquik`. store the api key in github secrets and use a repository variable for
the connected account name or id.

### message

the `message` input defines the content of your x post. this can be customized with dynamic content from your github workflow, such as:

- release version numbers
- commit information
- commit information
- repository data
- custom messages

### community-id

the optional `community-id` parameter allows you to post directly to a specific x community. if not provided, the post will appear on your main timeline only.
the optional `community-id` parameter allows you to post directly to a specific x community. if not provided, the post will appear on your main timeline only.
Loading