├── .all-contributorsrc ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── 01_help.md │ ├── 02_bug.md │ ├── 03_feature_request.md │ └── 04_thanks.md └── workflows │ ├── merge-schedule.yml │ ├── release.yml │ ├── test.yml │ └── twitter-together.yml ├── .gitignore ├── .nvmrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── action.yml ├── assets ├── demo.gif ├── logo.png └── logo.svg ├── dist └── index.js ├── docs ├── 01-create-twitter-app.md ├── 02-create-twitter-together-workflow.md ├── twitter-01-repository-secrets.png ├── workflow-01-actions-tab.png ├── workflow-02-editor.png └── workflow-03-commit.png ├── lib ├── common │ ├── parse-tweet-file-content.js │ └── tweet.js ├── index.js ├── pull-request │ ├── create-check-run.js │ ├── get-new-tweets.js │ └── index.js └── push │ ├── add-comment.js │ ├── get-new-tweets.js │ ├── index.js │ ├── is-setup-done.js │ └── setup.js ├── package-lock.json ├── package.json ├── test ├── command-line-has-invalid-tweet │ └── test.js ├── command-line-has-no-tweet │ └── test.js ├── command-line-has-tweet-with-front-matter-media-in-directory │ ├── custom │ │ ├── media │ │ │ └── blahaj.png │ │ └── tweets │ │ │ └── hello-world.tweet │ └── test.js ├── command-line-has-tweet-with-front-matter-media │ ├── media │ │ └── blahaj.png │ ├── test.js │ └── tweets │ │ └── hello-world.tweet ├── command-line-has-tweet │ ├── test.js │ └── tweets │ │ └── hello-world.tweet ├── pull-request-from-fork-has-tweet │ ├── event.json │ └── test.js ├── pull-request-from-fork-invalid-tweet │ ├── event.json │ └── test.js ├── pull-request-has-tweet-no-newline │ ├── event.json │ └── test.js ├── pull-request-has-tweet-with-poll-with-5-options │ ├── event.json │ └── test.js ├── pull-request-has-tweet-with-poll │ ├── event.json │ └── test.js ├── pull-request-has-tweet │ ├── event.json │ └── test.js ├── pull-request-invalid-tweet │ ├── event.json │ └── test.js ├── pull-request-no-tweets │ ├── event.json │ └── test.js ├── pull-request-request-error │ ├── event.json │ └── test.js ├── pull-request-wrong-base │ ├── event.json │ └── test.js ├── push-main-has-tweet-with-front-matter-media │ ├── event.json │ ├── media │ │ └── blahaj.png │ ├── test.js │ └── tweets │ │ └── hello-world.tweet ├── push-main-has-tweet-with-front-matter-poll │ ├── event.json │ ├── test.js │ └── tweets │ │ └── hello-world.tweet ├── push-main-has-tweet-with-front-matter-quote │ ├── event.json │ ├── test.js │ └── tweets │ │ └── hello-world.tweet ├── push-main-has-tweet-with-front-matter-reply │ ├── event.json │ ├── test.js │ └── tweets │ │ └── hello-world.tweet ├── push-main-has-tweet-with-front-matter-retweet │ ├── event.json │ ├── test.js │ └── tweets │ │ └── hello-world.tweet ├── push-main-has-tweet-with-poll │ ├── event.json │ ├── test.js │ └── tweets │ │ └── my-poll.tweet ├── push-main-has-tweet-with-thread │ ├── event.json │ ├── test.js │ └── tweets │ │ └── hello-world.tweet ├── push-main-has-tweet-with-trailing-whitespace │ ├── event.json │ ├── test.js │ └── tweets │ │ └── hello-world.tweet ├── push-main-has-tweet │ ├── event.json │ ├── test.js │ └── tweets │ │ └── hello-world.tweet ├── push-main-no-tweets │ ├── event.json │ ├── test.js │ └── tweets │ │ └── hello-world.tweet ├── push-main-request-error │ ├── event.json │ ├── test.js │ └── tweets │ │ └── hello-world.tweet ├── push-main-setup-pending │ ├── event.json │ └── test.js ├── push-main-setup │ ├── event.json │ └── test.js ├── push-main-tweet-error │ ├── event.json │ ├── test.js │ └── tweets │ │ └── cupcake-ipsum.tweet ├── push-not-a-branch │ ├── event.json │ └── test.js └── push-not-default-branch │ ├── event.json │ └── test.js └── tweets ├── 2019 ├── 02 │ └── twitter-together.tweet ├── 03 │ ├── Can I do this from here.tweet │ ├── nock.tweet │ └── probot.tweet ├── happy-new-year.tweet └── v2.tweet ├── 2020 ├── twitter-api-v7.tweet └── twitter-api-v8.tweet ├── 2021 ├── look-pops-we-made-it-on-github.tweet └── sample.tweet ├── README.md ├── ag-poll.tweet ├── hello-from-si.tweet ├── hello-world-horacioh.tweet ├── hello-world.tweet ├── johnb-test.tweet ├── kati-tweet.tweet ├── ktweet-test.tweet ├── my-first.tweet ├── poll-take-5.tweet ├── poll-take-6.tweet ├── poll-take-7.tweet ├── poll.tweet ├── prim4t.tweet ├── self_ref.tweet ├── test-davide.tweet └── test ├── debugging-squash-merge.tweet ├── does-this-still-work.tweet ├── fork.tweet ├── hello-world.tweet ├── multiline.tweet ├── progress.tweet └── username-and-hashtag.tweet /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "action", 3 | "projectOwner": "twitter-together", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": false, 11 | "contributors": [ 12 | { 13 | "login": "JasonEtco", 14 | "name": "Jason Etcovitch", 15 | "avatar_url": "https://avatars1.githubusercontent.com/u/10660468?v=4", 16 | "profile": "https://jasonet.co", 17 | "contributions": [ 18 | "design", 19 | "doc", 20 | "code" 21 | ] 22 | }, 23 | { 24 | "login": "Eronmmer", 25 | "name": "Erons", 26 | "avatar_url": "https://avatars0.githubusercontent.com/u/37238033?v=4", 27 | "profile": "http://erons.me", 28 | "contributions": [ 29 | "doc" 30 | ] 31 | }, 32 | { 33 | "login": "MattIPv4", 34 | "name": "Matt Cowley", 35 | "avatar_url": "https://avatars.githubusercontent.com/u/12371363?v=4", 36 | "profile": "https://mattcowley.co.uk/", 37 | "contributions": [ 38 | "code", 39 | "doc", 40 | "test", 41 | "ideas" 42 | ] 43 | } 44 | ], 45 | "contributorsPerLine": 7 46 | } 47 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [{*.js,*.json}] 2 | indent_size = 2 3 | indent_style = space 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | [test/push-main-has-tweet-with-trailing-whitespace/tweets/hello-world.tweet] 9 | trim_trailing_whitespace = false 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01_help.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🆘 Help" 3 | about: "How does this even work 🤷‍♂️" 4 | labels: support 5 | --- -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02_bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🐛 Bug Report" 3 | about: "If something isn't working as expected 🤔" 4 | labels: bug 5 | --- 6 | 7 | 8 | 9 | **What happened?** 10 | 11 | 12 | 13 | **What did you expect to happen?** 14 | 15 | 16 | 17 | **What the problem might be** 18 | 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/03_feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🧚‍♂️ Feature Request" 3 | about: "Wouldn’t it be nice if 💭" 4 | labels: feature 5 | --- 6 | 7 | 8 | 9 | **What’s missing?** 10 | 11 | 12 | 13 | **Why?** 14 | 15 | 16 | 17 | **Alternatives you tried** 18 | 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/04_thanks.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "💝 Thank you" 3 | about: "twitter-together is awesome 🙌" 4 | labels: thanks 5 | --- 6 | 7 | 8 | 9 | **How do you use twitter-together?** 10 | 11 | 12 | 13 | **What do you love about it?** 14 | 15 | 16 | 17 | **How did you learn about it?** 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/merge-schedule.yml: -------------------------------------------------------------------------------- 1 | name: Merge Schedule 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - edited 7 | schedule: 8 | # https://crontab.guru/every-hour 9 | - cron: 0 * * * * 10 | 11 | jobs: 12 | merge_schedule: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: gr2m/merge-schedule-action@v2 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | "on": 2 | push: 3 | branches: 4 | - main 5 | name: release 6 | jobs: 7 | release: 8 | name: release 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version-file: .nvmrc 15 | - run: npm install --no-save @semantic-release/git @semantic-release/exec 16 | - run: npx semantic-release 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GR2M_PAT_FOR_SEMANTIC_RELEASE }} 19 | - run: >- 20 | git push --force 21 | https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git 22 | HEAD:refs/heads/v3 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GR2M_PAT_FOR_SEMANTIC_RELEASE }} 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | "on": 3 | pull_request: 4 | types: 5 | - opened 6 | - synchronize 7 | jobs: 8 | test: 9 | name: Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Setup node 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version-file: .nvmrc 17 | - run: npm ci 18 | - run: npm run build 19 | - run: npm test 20 | -------------------------------------------------------------------------------- /.github/workflows/twitter-together.yml: -------------------------------------------------------------------------------- 1 | # NOTE: if you want to use this file as a template, make sure to replace 'main' in line 21 2 | # with your repository's default branch (in case you changed it to something other than 'main') 3 | 4 | on: [push, pull_request] 5 | name: Twitter, together! 6 | jobs: 7 | preview: 8 | name: Preview 9 | if: github.event_name == 'pull_request' 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version-file: .nvmrc 16 | - run: npm ci 17 | - run: npm run build 18 | - name: Preview 19 | uses: ./ 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | tweet: 23 | name: Tweet 24 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: actions/setup-node@v3 29 | with: 30 | node-version-file: .nvmrc 31 | - run: npm ci 32 | - run: npm run build 33 | - name: Tweet 34 | uses: ./ 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} 38 | TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} 39 | TWITTER_API_KEY: ${{ secrets.TWITTER_API_KEY }} 40 | TWITTER_API_SECRET_KEY: ${{ secrets.TWITTER_API_SECRET_KEY }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .nyc_output 3 | coverage/ 4 | dist/ 5 | node_modules/ 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at twitter-together+coc@martynus.net. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. 4 | 5 | 6 | 7 | - [Have a question? Found a bug? Have an idea?](#have-a-question-found-a-bug-have-an-idea) 8 | - [Submitting the Pull Request](#submitting-the-pull-request) 9 | - [Merging the Pull Request & releasing a new version](#merging-the-pull-request--releasing-a-new-version) 10 | - [Resources](#resources) 11 | 12 | 13 | 14 | ## Have a question? Found a bug? Have an idea? 15 | 16 | Please [create an issue](https://github.com/twitter-together/action/issues/new/choose). 17 | 18 | I love pull requests 😍 but before you put in too much time I’d appreciate if you created an issue first to make sure that it is an actual issue. 19 | 20 | ## Submitting the Pull Request 21 | 22 | If you would like to contribute a bug fix or new feature (after discussing in an issue), please add tests. 23 | 24 | Each test is a folder such as [`test/push-main-has-tweet`](https://github.com/twitter-together/action/tree/main/test/push-main-has-tweet). You can either adapt one of the existing tests or create a new folder by copying it. 25 | 26 | Each folder has a `test.js` file which runs the test, an `event.json` file which has the payload for the event you want to simulate and any other files that simulate a certain state a repository would be in. 27 | 28 | You can run the tests using `npm test`. You can run a single test using `npx tap test/`. 29 | 30 | ## Merging the Pull Request & releasing a new version 31 | 32 | Releases are automated using [semantic-release](https://github.com/semantic-release/semantic-release). 33 | The following commit message conventions determine which version is released: 34 | 35 | 1. `fix: ...` or `fix(scope name): ...` prefix in subject: bumps fix version, e.g. `1.2.3` → `1.2.4` 36 | 2. `feat: ...` or `feat(scope name): ...` prefix in subject: bumps feature version, e.g. `1.2.3` → `1.3.0` 37 | 3. `BREAKING CHANGE: ` in body: bumps breaking version, e.g. `1.2.3` → `2.0.0` 38 | 39 | Only one version number is bumped at a time, the highest version change trumps the others. 40 | 41 | If the pull request looks good but does not follow the commit conventions, use the "Squash & merge" button. 42 | 43 | ## Resources 44 | 45 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 46 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) 47 | - [GitHub Help](https://help.github.com) 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Gregor Martynus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | twitter together logo 3 |

4 | 5 |

Twitter, together!

6 | 7 |

8 | Build Status 9 | Coverage 10 |

11 | 12 | For Open Source or event maintainers that share a project twitter account, `twitter-together` is a GitHub Action that utilizes text files to publish tweets from a GitHub repository. Rather than tweeting directly, GitHub’s pull request review process encourages more collaboration, Twitter activity and editorial contributions by enabling everyone to submit tweet drafts to a project. 13 | 14 |

15 | Screencast demonstrating twitter-together 16 |

17 | 18 | 19 | 20 | - [Try it](#try-it) 21 | - [Twitter API compatibility](#twitter-api-compatibility) 22 | - [Setup](#setup) 23 | - [Contribute](#contribute) 24 | - [How it works](#how-it-works) 25 | - [The `push` event](#the-push-event) 26 | - [The `pull_request` event](#the-pull_request-event) 27 | - [Advanced tweeting](#advanced-tweeting) 28 | - [Motivation](#motivation) 29 | - [License](#license) 30 | 31 | 32 | 33 | ## Try it 34 | 35 | You can submit a tweet to this repository to see the magic happen. Please follow the instructions at [tweets/README.md](tweets/README.md) and mention your own twitter username to the tweet. This repository is setup to tweet from [https://twitter.com/commit2tweet](https://twitter.com/commit2tweet). 36 | 37 | ## Twitter API compatibility 38 | 39 | Twitter, Together uses the v2 Twitter API for most functionality. 40 | It makes use of the v1 API for media uploads, as there is no v2 equivalent endpoint. 41 | 42 | Essentials level Twitter access should grant access to all endpoints Twitter, Together uses. 43 | 44 | ## Setup 45 | 46 | Unless you wish to contribute to this project, you don't need to fork this repository. 47 | Instead, you can make use of this GitHub Action from the comfort of your own repository (either a new one, or one you already have) by creating a GitHub Actions workflow following these steps: 48 | 49 | 1. [Create a Twitter app](docs/01-create-twitter-app.md) with your shared Twitter account and store the credentials as `TWITTER_API_KEY`, `TWITTER_API_SECRET_KEY`, `TWITTER_ACCESS_TOKEN` and `TWITTER_ACCESS_TOKEN_SECRET` in your repository’s secrets settings. 50 | 2. [Create a `.github/workflows/twitter-together.yml` file](docs/02-create-twitter-together-workflow.md) with the content below. Make sure to replace `'main'` if you changed your repository's default branch. 51 | 52 | ```yml 53 | on: [push, pull_request] 54 | name: Twitter, together! 55 | jobs: 56 | preview: 57 | name: Preview 58 | runs-on: ubuntu-latest 59 | if: github.event_name == 'pull_request' 60 | steps: 61 | - uses: twitter-together/action@v3 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 64 | tweet: 65 | name: Tweet 66 | runs-on: ubuntu-latest 67 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 68 | steps: 69 | - name: checkout main 70 | uses: actions/checkout@v3 71 | - name: Tweet 72 | uses: twitter-together/action@v3 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} 76 | TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} 77 | TWITTER_API_KEY: ${{ secrets.TWITTER_API_KEY }} 78 | TWITTER_API_SECRET_KEY: ${{ secrets.TWITTER_API_SECRET_KEY }} 79 | ``` 80 | 81 | 3. After creating or updating `.github/workflows/twitter-together.yml` in your repository’s default branch, a pull request will be created with further instructions. 82 | 83 | Happy collaborative tweeting! 84 | 85 | ## Contribute 86 | 87 | All contributions welcome! 88 | 89 | Especially if you try `twitter-together` for the first time, I’d love to hear if you ran into any trouble. I greatly appreciate any documentation improvements to make things more clear, I am not a native English speaker myself. 90 | 91 | See [CONTRIBUTING.md](CONTRIBUTING.md) for more information on how to contribute. You can also [just say thanks](https://github.com/twitter-together/action/issues/new?labels=feature&template=04_thanks.md) 😊 92 | 93 | ## Thanks to all contributors 💐 94 | 95 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 |

Jason Etcovitch

🎨 📖 💻

Erons

📖

Matt Cowley

💻 📖 ⚠️ 🤔
109 | 110 | 111 | 112 | 113 | 114 | 115 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 116 | 117 | ## How it works 118 | 119 | `twitter-together` is using two workflows 120 | 121 | 1. `push` event to publish new tweets 122 | 2. `pull_request` event to validate and preview new tweets 123 | 124 | _(Tweets can also be invoked locally by calling the script with the `--file` flag, which can be useful for development. E.g. `TWITTER_ACCESS_TOKEN=... node lib/index.js --file tweets/hello-world.tweet`)_ 125 | 126 | ### The `push` event 127 | 128 | When triggered by the `push` event, the script looks for added `*.tweet` files in the `tweets/` folder or subfolders. If there are any, a tweet for each added tweet file is published. 129 | 130 | If there is no `tweets/` subfolder, the script opens a pull request creating the folder with further instructions. 131 | 132 | ### The `pull_request` event 133 | 134 | For the `pull_request` event, the script handles only `opened` and `synchronize` actions. It looks for new `*.tweet` files in the `tweets/` folder or subfolders. If there are any, the length of each tweet is validated. If one is too long, a failed check run with an explanation is created. If all tweets are valid, a check run with a preview of all tweets is created. 135 | 136 | ### Advanced tweeting 137 | 138 | Beyond tweeting out plain-text tweets, twitter-together also supports creating polls, replying to other tweets, retweeting or quote-retweeting other tweets, threading a chain of tweets, and adding images to tweets. 139 | 140 | Polls can be included directly in the body of tweet like so: 141 | 142 | ```tweet 143 | What is your favorite color? 144 | 145 | ( ) Red 146 | ( ) Blue 147 | ( ) Green 148 | ``` 149 | 150 | All other advanced tweeting features are supporting through defining YAML frontmatter in the tweet file. 151 | Some frontmatter items can be combined together, where Twitter functionality supports it. 152 | 153 | A poll can also be defined in frontmatter, rather than in the tweet body, like so: 154 | 155 | ```tweet 156 | --- 157 | poll: 158 | - Red 159 | - Blue 160 | - Green 161 | --- 162 | 163 | What is your favorite color? 164 | ``` 165 | 166 | To reply to another tweet, include the `reply` frontmatter item with the tweet link that you wish to reply to: 167 | 168 | ```tweet 169 | --- 170 | reply: https://twitter.com/gr2m/status/1409601188362809349 171 | --- 172 | 173 | @gr2m I love your work! 174 | ``` 175 | 176 | If you want to quote-retweet another tweet, include the `retweet` frontmatter item with the tweet link that you wish to quote-retweet. 177 | If you'd prefer to just retweet without quoting, don't provide a tweet body after the frontmatter. 178 | 179 | ```tweet 180 | --- 181 | retweet: https://twitter.com/gr2m/status/1409601188362809349 182 | --- 183 | 184 | twitter-together is awesome! 185 | ``` 186 | 187 | To include media items with your tweet, include the `media` frontmatter item as an array with each item having a `file` property and an optional `alt` property. 188 | The `file` property should be the name of a file within the `media` directory of your repository (same level as the `tweets` directory). 189 | 190 | _(Note: Although alt text can be set in frontmatter, it is not yet actually passed to Twitter due to library limitations)._ 191 | 192 | ```tweet 193 | --- 194 | media: 195 | - file: cat.jpg 196 | alt: A cat 197 | - file: dog.jpg 198 | alt: A dog 199 | --- 200 | 201 | Here are some cute animals! 202 | ``` 203 | 204 | To thread a chain of tweets, use `---` to delimit each tweet in the file. You can optionally set `threadDelimiter` in the frontmatter to change the delimiter for the next tweet in the thread. Each tweet in a thread supports its own frontmatter. 205 | 206 | ```tweet 207 | --- 208 | media: 209 | - file: cat.jpg 210 | alt: A cat 211 | - file: dog.jpg 212 | alt: A dog 213 | --- 214 | 215 | Here are some cute animals! 216 | 217 | --- 218 | --- 219 | poll: 220 | - Cat 221 | - Dog 222 | --- 223 | 224 | Which one is cuter? 225 | ``` 226 | 227 | ## Motivation 228 | 229 | I think we can make Open Source more inclusive to people with more diverse interests by making it easier to contribute other things than code and documentation. I see a particularly big opportunity to be more welcoming towards editorial contributions by creating tools using GitHub’s Actions, Apps and custom user interfaces backed by GitHub’s REST & GraphQL APIs. 230 | 231 | I’ve plenty more ideas that I’d like to build out. Please ping me on twitter if you’d like to chat: [@gr2m](https://twitter.com/gr2m). 232 | 233 | ## License 234 | 235 | [MIT](LICENSE) 236 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Twitter, together! 2 | description: "Collaborate on tweets just like you collaborate on code, using pull requests" 3 | branding: 4 | icon: cast 5 | color: blue 6 | runs: 7 | using: "node20" 8 | main: "dist/index.js" 9 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twitter-together/action/08857009da2aacd9bd08204550ec96e15d76b4da/assets/demo.gif -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twitter-together/action/08857009da2aacd9bd08204550ec96e15d76b4da/assets/logo.png -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | twitter-together -------------------------------------------------------------------------------- /docs/01-create-twitter-app.md: -------------------------------------------------------------------------------- 1 | [back to README.md](../README.md/#setup) 2 | 3 | # Create a Twitter App 4 | 5 | Head to to begin the process of creating a Twitter app. 6 | 7 | If you haven’t yet, you will be asked to apply for a Twitter developer account. 8 | If you’ve done that before, skip the next section and continue at [Create an app](#create-an-app). 9 | 10 | ## Apply for a developer account 11 | 12 | You might be asked to add a phone number to your Twitter account before proceeding. 13 | If the phone number is used in another account, it won’t let you use it again 14 | But you can remove the phone number from the other account. 15 | You can change it back once your developer account was approved. 16 | 17 | Your Twitter account will also need to be associated with an email address. 18 | If it isn't yet, set the email address in your Twitter account [email settings](https://twitter.com/settings/email). 19 | 20 | During the account creation, Twitter tends to ask about what you'll be doing with the developer account. 21 | If you aren't sure what to answer, here are our recommend answers: 22 | 23 | - What's your use case? Select "Making a bot". 24 | - Will you make Twitter content or derived information available to a government entity or a government affiliated entity? Select "No". 25 | 26 | Once you've submitted the form, you will receive an email to verify your developer account. 27 | After that you can head back to to begin creating your app. 28 | 29 | --- 30 | 31 | ## Create an App 32 | 33 | ### Project creation 34 | 35 | Before we create the app itself, we need to create a project that will house the app. 36 | If you've already got a project in your developer account that you want to use, skip this step. 37 | Head to or click "Create Project" on your dashboard. 38 | 39 | #### Project name 40 | 41 | You can provide any name you want for your project here. 42 | We recommend keeping the name related to Twitter, Together, to avoid future confusion. 43 | 44 | #### Project use case 45 | 46 | If unsure, select "Making a bot" for the project use case. 47 | 48 | #### Project description 49 | 50 | You can provide any description you want for your project here. 51 | If you're creating this project for Twitter, Together only, you could use: 52 | 53 | > Collaboratively tweet using GitHub’s pull request review process by utilizing the twitter-together GitHub Action. 54 | 55 | ### App creation 56 | 57 | With the project created, we can now create the app itself. 58 | If you've gone through the project creation flow immediately before, Twitter may automatically take you to the app creation flow. 59 | If not, head back to your [dashboard](https://developer.twitter.com/en/portal/projects-and-apps) and look for a "Add App" button under your project. 60 | 61 | #### App environment 62 | 63 | Depending on your level of access to the Twitter API, the app creation flow may or may not ask you for this step. 64 | 65 | If you are asked for the app environment, select the environment the best suites how you'll be using Twitter, Together. 66 | 67 | #### App name 68 | 69 | Twitter app names are globally unique, so you'll want to provide a name here that makes sense in the context of your Twitter account or intended usage. 70 | We recommend going for `-twitter-together`, e.g. `probot-twitter-together`. 71 | 72 | Once you've provided the app name, Twitter will present you with credentials for your app. 73 | Note down the "API Key" and "API Secret" as we'll need them later. 74 | 75 | #### App settings 76 | 77 | With your app created, head to the settings for the app as we'll need to ensure it has write access as well as the default read access. 78 | Twitter seems to change this UI far too often, but as of writing, to enable write access you'll need to configure "User authentication settings". 79 | 80 | Press "Set up" under the "User authentication settings" section. Select "Read and write" under "App permissions" and "Web App, Automated App or Bot" under "Type of App". 81 | 82 | Now we'll need to configure a fake OAuth 2.0 flow (which we won't use): 83 | 84 | - App info -> Callback URI: http://localhost 85 | - App info -> Website URL: https://github.com/twitter-together/action 86 | 87 | With those all set, press "Save" and confirm that you are happy to change the permissions for your app. 88 | Disregard the client ID and secret presented, as we won't actually be using OAuth. 89 | 90 | ### Save credentials 91 | 92 | Head back to your app settings, and jump into the "Keys and tokens" tab. 93 | 94 | If you forgot to note down the API Key/Secret earlier, no worries! 95 | Press "Regenerate" next to "API Key and Secret" and Twitter will give you a new pair. 96 | 97 | We'll also want to press "Regenerate" next to the "Access Token and Secret" as we need to update the permissions it has to be both read and write. 98 | Note down the token and token secret Twitter gives you. 99 | 100 | Now save the credentials into your repository’s "Secrets" settings as follows: 101 | 102 | | Twitter Credential name | GitHub Secret name | 103 | | ----------------------- | ----------------------------- | 104 | | API key | `TWITTER_API_KEY` | 105 | | API secret key | `TWITTER_API_SECRET_KEY` | 106 | | Access token | `TWITTER_ACCESS_TOKEN` | 107 | | Access token secret | `TWITTER_ACCESS_TOKEN_SECRET` | 108 | 109 | ![](twitter-01-repository-secrets.png) 110 | 111 | --- 112 | 113 | You're all set! :tada: 114 | 115 | Next: [Create a `.github/workflows/twitter-together.yml` file](02-create-twitter-together-workflow.md) 116 | -------------------------------------------------------------------------------- /docs/02-create-twitter-together-workflow.md: -------------------------------------------------------------------------------- 1 | [back to README.md](../README.md/#setup) 2 | 3 | # Create a `.github/workflows/twitter-together.yml` file 4 | 5 | In your repository, open the Actions tab. 6 | 7 | ![](workflow-01-actions-tab.png) 8 | 9 | Press the Setup a new workflow yourself button to open the file editor. 10 | 11 | ![](workflow-02-editor.png) 12 | 13 | In the filename input above the code area, replace `main.yml` with `twitter-together.yml`. Then replace the code: 14 | 15 | ```yml 16 | on: [push, pull_request] 17 | name: Twitter, together! 18 | jobs: 19 | preview: 20 | name: Preview 21 | runs-on: ubuntu-latest 22 | if: github.event_name == 'pull_request' 23 | steps: 24 | - uses: twitter-together/action@v3 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | tweet: 28 | name: Tweet 29 | runs-on: ubuntu-latest 30 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 31 | steps: 32 | - name: checkout main 33 | uses: actions/checkout@v3 34 | - name: Tweet 35 | uses: twitter-together/action@v3 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} 39 | TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} 40 | TWITTER_API_KEY: ${{ secrets.TWITTER_API_KEY }} 41 | TWITTER_API_SECRET_KEY: ${{ secrets.TWITTER_API_SECRET_KEY }} 42 | ``` 43 | 44 | Make sure to replace `'main'` if you changed your repository's default branch. 45 | 46 | ![](workflow-04-commit.png) 47 | 48 | To create the file, press the Start commit button. You can optionally set a custom commit message, then press Commit new file. 49 | 50 | --- 51 | 52 | Nearly done! Shortly after creating or updating `.github/workflows/twitter-together.yml` in your repository’s default branch, a pull request will be created with further instructions. 53 | 54 | [back to README.md](../README.md/#setup) 55 | -------------------------------------------------------------------------------- /docs/twitter-01-repository-secrets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twitter-together/action/08857009da2aacd9bd08204550ec96e15d76b4da/docs/twitter-01-repository-secrets.png -------------------------------------------------------------------------------- /docs/workflow-01-actions-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twitter-together/action/08857009da2aacd9bd08204550ec96e15d76b4da/docs/workflow-01-actions-tab.png -------------------------------------------------------------------------------- /docs/workflow-02-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twitter-together/action/08857009da2aacd9bd08204550ec96e15d76b4da/docs/workflow-02-editor.png -------------------------------------------------------------------------------- /docs/workflow-03-commit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twitter-together/action/08857009da2aacd9bd08204550ec96e15d76b4da/docs/workflow-03-commit.png -------------------------------------------------------------------------------- /lib/common/parse-tweet-file-content.js: -------------------------------------------------------------------------------- 1 | module.exports = parseTweetFileContent; 2 | 3 | const EOL = require("os").EOL; 4 | 5 | const { existsSync } = require("fs"); 6 | const { join } = require("path"); 7 | const { parseTweet } = require("twitter-text"); 8 | const { load } = require("js-yaml"); 9 | 10 | const OPTION_REGEX = /^\(\s?\)\s+/; 11 | const FRONT_MATTER_REGEX = new RegExp( 12 | `^---[ \t]*${EOL}([\\s\\S]*?)${EOL}---[ \t]*(?:$|(?:${EOL})+)` 13 | ); 14 | 15 | function parseTweetFileContent(text, dir, isThread = false) { 16 | text = text.trim(); 17 | 18 | const options = { 19 | threadDelimiter: "---", 20 | reply: null, 21 | retweet: null, 22 | media: [], 23 | schedule: null, 24 | poll: null, 25 | thread: null, 26 | }; 27 | 28 | // Extract front matter options 29 | const frontMatterMatch = text.match(FRONT_MATTER_REGEX); 30 | if (frontMatterMatch) { 31 | text = text.slice(frontMatterMatch[0].length); 32 | getOptionsFromFrontMatter(frontMatterMatch[1], options, dir); 33 | 34 | if (isThread) { 35 | if (options.reply) 36 | throw new Error("Cannot set a tweet to reply to when in a thread"); 37 | } 38 | } 39 | 40 | // Handle threading 41 | if (options.threadDelimiter) { 42 | const threadIdx = text.match( 43 | new RegExp(`(?:${EOL})+${options.threadDelimiter}[ \t]*(?:${EOL})+`) 44 | ); 45 | if (threadIdx) { 46 | const threadText = text.slice(threadIdx.index + threadIdx[0].length); 47 | text = text.slice(0, threadIdx.index); 48 | 49 | // Each item can have front matter, as we only split one thread delimiter at a time 50 | options.thread = parseTweetFileContent(threadText, dir, true); 51 | } 52 | } 53 | 54 | // Extract in-content options 55 | if (!options.poll) { 56 | const pollOptions = []; 57 | let lastLine; 58 | while ((lastLine = getlastLineMatchingPollOption(text))) { 59 | pollOptions.push(lastLine.replace(OPTION_REGEX, "")); 60 | text = withLastLineRemoved(text); 61 | } 62 | if (pollOptions.length) options.poll = pollOptions.reverse(); 63 | } 64 | 65 | // Validate options 66 | validateOptions(options, text, dir); 67 | 68 | // Parse tweet if has text 69 | const parsed = text ? parseTweet(text) : { valid: true, weightedLength: 0 }; 70 | if (!parsed.valid) 71 | throw new Error( 72 | `Tweet exceeds maximum length of 280 characters by ${ 73 | parsed.weightedLength - 280 74 | } characters` 75 | ); 76 | 77 | // TODO: Support schedule from options 78 | return { 79 | poll: options.poll, 80 | media: options.media, 81 | thread: options.thread, 82 | reply: options.reply, 83 | retweet: options.retweet, 84 | text, 85 | ...parsed, 86 | }; 87 | } 88 | 89 | function validateOptions(options, text, dir) { 90 | if (options.retweet && !text && options.poll) 91 | throw new Error("Cannot attach a poll to a retweet"); 92 | 93 | if (options.retweet && !text && options.reply) 94 | throw new Error("Cannot reply to a tweet with a retweet"); 95 | 96 | if (options.retweet && !text && options.thread) 97 | throw new Error("Cannot create a thread from a retweet"); 98 | 99 | if (options.retweet && !text && options.media && options.media.length) 100 | throw new Error("Cannot attach media to a retweet"); 101 | 102 | if (options.poll && options.poll.length > 4) 103 | throw new Error( 104 | `Polls cannot have more than four options, found ${options.poll.length} options` 105 | ); 106 | 107 | if (options.poll && options.poll.length < 2) 108 | throw new Error( 109 | `Polls must have at least two options, found ${options.poll.length} options` 110 | ); 111 | 112 | if (options.media) { 113 | for (const media of options.media) { 114 | if (media.file.indexOf(join(dir, "media")) !== 0) 115 | throw new Error(`Media file should be within the media directory`); 116 | 117 | if (!existsSync(media.file)) 118 | throw new Error(`Media file ${media.file} does not exist`); 119 | 120 | if (media.alt && media.alt.length > 1000) 121 | throw new Error( 122 | `Media alt text must be 1000 characters or less, found length ${media.alt.length}` 123 | ); 124 | } 125 | } 126 | } 127 | 128 | function getOptionsFromFrontMatter(frontMatter, options, dir) { 129 | const parsedFrontMatter = load(frontMatter); 130 | if (typeof parsedFrontMatter !== "object" || !parsedFrontMatter) return; 131 | 132 | if (typeof parsedFrontMatter["thread-delimiter"] === "string") 133 | options.threadDelimiter = parsedFrontMatter["thread-delimiter"]; 134 | if (typeof parsedFrontMatter.reply === "string") 135 | options.reply = parsedFrontMatter.reply; 136 | if (typeof parsedFrontMatter.retweet === "string") 137 | options.retweet = parsedFrontMatter.retweet; 138 | 139 | if (Array.isArray(parsedFrontMatter.media)) 140 | options.media = parsedFrontMatter.media.reduce((arr, item) => { 141 | if (item && typeof item === "object" && typeof item.file === "string") 142 | arr.push({ 143 | file: join(dir, "media", item.file), 144 | alt: typeof item.alt !== "string" ? null : item.alt, 145 | }); 146 | return arr; 147 | }, []); 148 | 149 | if (typeof parsedFrontMatter.schedule === "string") { 150 | const schedule = new Date(parsedFrontMatter.schedule); 151 | if (!isNaN(schedule.getTime())) options.schedule = schedule; 152 | } 153 | 154 | if (Array.isArray(parsedFrontMatter.poll)) 155 | options.poll = parsedFrontMatter.poll.reduce((arr, item) => { 156 | if (typeof item === "string") arr.push(item); 157 | return arr; 158 | }, []); 159 | } 160 | 161 | function getlastLineMatchingPollOption(text) { 162 | const lines = text.trim().split(EOL); 163 | const [lastLine] = lines.reverse(); 164 | return OPTION_REGEX.test(lastLine) ? lastLine : null; 165 | } 166 | 167 | function withLastLineRemoved(text) { 168 | const lines = text.trim().split(EOL); 169 | return lines 170 | .slice(0, lines.length - 1) 171 | .join(EOL) 172 | .trim(); 173 | } 174 | -------------------------------------------------------------------------------- /lib/common/tweet.js: -------------------------------------------------------------------------------- 1 | module.exports = tweet; 2 | 3 | const { TwitterApi } = require("twitter-api-v2"); 4 | const mime = require("mime-types"); 5 | 6 | const TWEET_REGEX = /^https:\/\/twitter\.com\/[^/]+\/status\/(\d+)$/; 7 | 8 | async function tweet({ twitterCredentials }, tweetData, tweetFile) { 9 | const client = new TwitterApi(twitterCredentials); 10 | 11 | const self = await client.v2.me(); 12 | if (self.errors) throw self.errors; 13 | 14 | return handleTweet(client, self.data, tweetData, tweetFile); 15 | } 16 | 17 | async function handleTweet(client, self, tweet, name) { 18 | if (tweet.retweet && !tweet.text) { 19 | // TODO: Should this throw if an invalid tweet is passed and there is no match? 20 | const match = tweet.retweet.match(TWEET_REGEX); 21 | if (match) return createRetweet(client, self, match[1]); 22 | } 23 | 24 | const tweetData = { 25 | text: tweet.text, 26 | }; 27 | 28 | if (tweet.poll) { 29 | tweetData.poll = { 30 | duration_minutes: 1440, 31 | options: tweet.poll, 32 | }; 33 | } 34 | 35 | if (tweet.reply) { 36 | // TODO: Should this throw if an invalid reply is passed and there is no match? 37 | const match = tweet.reply.match(TWEET_REGEX); 38 | if (match) { 39 | tweetData.reply = { 40 | in_reply_to_tweet_id: match[1], 41 | }; 42 | } 43 | } 44 | 45 | if (tweet.retweet) { 46 | // TODO: Should this throw if an invalid tweet is passed and there is no match? 47 | const match = tweet.retweet.match(TWEET_REGEX); 48 | if (match) tweetData.quote_tweet_id = match[1]; 49 | } 50 | 51 | if (tweet.media?.length) { 52 | tweetData.media = { 53 | media_ids: await Promise.all( 54 | tweet.media.map((media) => createMedia(client, media)) 55 | ), 56 | }; 57 | } 58 | 59 | const tweetResult = await createTweet(client, self, tweetData); 60 | 61 | if (tweet.thread) { 62 | tweetResult.thread = await handleTweet( 63 | client, 64 | self, 65 | { ...tweet.thread, reply: tweetResult.url }, 66 | name 67 | ); 68 | } 69 | 70 | return tweetResult; 71 | } 72 | 73 | async function createMedia(client, { file, alt }) { 74 | const mediaId = await client.v1.uploadMedia(file, { 75 | mimeType: mime.lookup(file), 76 | }); 77 | if (alt) 78 | await client.v1.createMediaMetadata(mediaId, { alt_text: { text: alt } }); 79 | return mediaId; 80 | } 81 | 82 | async function createTweet(client, self, options) { 83 | return client.v2.tweet(options).then((data) => { 84 | if (data.errors) throw data.errors; 85 | return { 86 | text: data.data.text, 87 | url: `https://twitter.com/${self.username}/status/${data.data.id}`, 88 | }; 89 | }); 90 | } 91 | 92 | function createRetweet(client, self, id) { 93 | return client.v2.retweet(self.id, id).then(async (data) => { 94 | if (data.errors) throw data.errors; 95 | if (!data.data.retweeted) throw new Error("Retweet failed"); 96 | 97 | const other = await client.v2.singleTweet(id, { expansions: "author_id" }); 98 | if (other.errors) throw other.errors; 99 | const otherUser = other.includes.users.find( 100 | (user) => user.id === other.data.author_id 101 | ); 102 | 103 | return { 104 | retweet: `https://twitter.com/${otherUser.username}/status/${id}`, 105 | url: `https://twitter.com/${otherUser.username}/status/${id}`, // TODO: Twitter does not return the id of the retweet itself 106 | }; 107 | }); 108 | } 109 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require("fs"); 2 | const { resolve, basename } = require("path"); 3 | 4 | const { Octokit } = require("@octokit/action"); 5 | const toolkit = require("@actions/core"); 6 | 7 | const handlePullRequest = require("./pull-request"); 8 | const handlePush = require("./push"); 9 | const parseTweetFileContent = require("./common/parse-tweet-file-content"); 10 | const tweet = require("./common/tweet"); 11 | 12 | const VERSION = require("../package.json").version; 13 | 14 | console.log(`Running twitter-together version ${VERSION}`); 15 | 16 | async function main() { 17 | const state = { 18 | startedAt: new Date().toISOString(), 19 | twitterCredentials: { 20 | appKey: process.env.TWITTER_API_KEY, 21 | appSecret: process.env.TWITTER_API_SECRET_KEY, 22 | accessToken: process.env.TWITTER_ACCESS_TOKEN, 23 | accessSecret: process.env.TWITTER_ACCESS_TOKEN_SECRET, 24 | }, 25 | }; 26 | 27 | // Allow for CLI invocation using `--file tweets/test.tweet` 28 | if (process.argv.length > 2 && process.argv[2] === "--file") { 29 | if (!process.argv[3]) throw new Error("No file specified for --file"); 30 | const fileState = { 31 | ...state, 32 | dir: resolve(process.argv[3], "..", ".."), 33 | }; 34 | 35 | const payload = readFileSync(resolve(process.argv[3]), "utf8"); 36 | const parsed = parseTweetFileContent(payload, fileState.dir); 37 | console.log("Parsed tweet:", parsed); 38 | console.log(await tweet(fileState, parsed, basename(process.argv[3]))); 39 | return; 40 | } 41 | 42 | // If not given file flag, assume GitHub Action 43 | const payload = JSON.parse( 44 | readFileSync(process.env.GITHUB_EVENT_PATH, "utf8") 45 | ); 46 | const ref = process.env.GITHUB_REF; 47 | const sha = process.env.GITHUB_SHA; 48 | const dir = process.env.GITHUB_WORKSPACE; 49 | const githubState = { 50 | ...state, 51 | toolkit, 52 | octokit: new Octokit(), 53 | payload, 54 | ref, 55 | sha, 56 | dir, 57 | }; 58 | 59 | switch (process.env.GITHUB_EVENT_NAME) { 60 | case "push": 61 | await handlePush(githubState); 62 | break; 63 | case "pull_request": 64 | await handlePullRequest(githubState); 65 | break; 66 | } 67 | } 68 | 69 | main().catch((error) => { 70 | console.error(error); 71 | process.exit(1); 72 | }); 73 | -------------------------------------------------------------------------------- /lib/pull-request/create-check-run.js: -------------------------------------------------------------------------------- 1 | module.exports = createCheckRun; 2 | 3 | const { autoLink } = require("twitter-text"); 4 | 5 | const parseTweetFileContent = require("../common/parse-tweet-file-content"); 6 | 7 | async function createCheckRun( 8 | { octokit, payload, startedAt, toolkit, dir }, 9 | newTweets 10 | ) { 11 | const parsedTweets = newTweets.map((rawTweet) => { 12 | try { 13 | return parseTweetFileContent(rawTweet, dir); 14 | } catch (error) { 15 | return { 16 | error: error.message, 17 | valid: false, 18 | text: rawTweet, 19 | }; 20 | } 21 | }); 22 | 23 | const allTweetsValid = parsedTweets.every((tweet) => tweet.valid); 24 | 25 | // Check runs cannot be created if the pull request was created by a fork, 26 | // so we just log out the result. 27 | // https://help.github.com/en/actions/automating-your-workflow-with-github-actions/authenticating-with-the-github_token#permissions-for-the-github_token 28 | if (payload.pull_request.head.repo.fork) { 29 | for (const tweet of parsedTweets) { 30 | if (tweet.valid) { 31 | toolkit.info(`### ✅ Valid\n\n${tweet.text}`); 32 | } else { 33 | toolkit.info( 34 | `### ❌ Invalid\n\n${tweet.text}\n\n${tweet.error || "Unknown error"}` 35 | ); 36 | } 37 | } 38 | process.exit(allTweetsValid ? 0 : 1); 39 | } 40 | 41 | const response = await octokit.request( 42 | "POST /repos/:owner/:repo/check-runs", 43 | { 44 | headers: { 45 | accept: "application/vnd.github.antiope-preview+json", 46 | }, 47 | owner: payload.repository.owner.login, 48 | repo: payload.repository.name, 49 | name: "preview", 50 | head_sha: payload.pull_request.head.sha, 51 | started_at: startedAt, 52 | completed_at: new Date().toISOString(), 53 | status: "completed", 54 | conclusion: allTweetsValid ? "success" : "failure", 55 | output: { 56 | title: `${parsedTweets.length} tweet(s)`, 57 | summary: parsedTweets.map(tweetToCheckRunSummary).join("\n\n---\n\n"), 58 | }, 59 | } 60 | ); 61 | 62 | toolkit.info(`check run created: ${response.data.html_url}`); 63 | } 64 | 65 | function tweetToCheckRunSummary(tweet) { 66 | let text = autoLink(tweet.text) 67 | .replace(/(^|\n)/g, "$1> ") 68 | .replace(/(^|\n)> (\n|$)/g, "$1>$2"); 69 | 70 | if (!tweet.valid) 71 | return `### ❌ Invalid\n\n${text}\n\n${tweet.error || "Unknown error"}`; 72 | 73 | if (tweet.poll) 74 | text += 75 | "\n\nThe tweet includes a poll:\n\n> 🔘 " + tweet.poll.join("\n> 🔘 "); 76 | return `### ✅ Valid\n\n${text}`; 77 | } 78 | -------------------------------------------------------------------------------- /lib/pull-request/get-new-tweets.js: -------------------------------------------------------------------------------- 1 | module.exports = getNewTweets; 2 | 3 | const parseDiff = require("parse-diff"); 4 | 5 | async function getNewTweets({ octokit, toolkit, payload }) { 6 | // Avoid loading huuuge diffs for pull requests that don’t create a new tweet file 7 | const response = await octokit.request( 8 | "GET /repos/:owner/:repo/pulls/:number/files", 9 | { 10 | owner: payload.repository.owner.login, 11 | repo: payload.repository.name, 12 | number: payload.pull_request.number, 13 | } 14 | ); 15 | 16 | const { data: files } = response; 17 | 18 | const newTweet = files.find( 19 | (file) => 20 | file.status === "added" && /^tweets\/.*\.tweet$/.test(file.filename) 21 | ); 22 | 23 | if (!newTweet) { 24 | toolkit.info("Pull request does not include new tweets"); 25 | process.exit(0); 26 | } 27 | 28 | toolkit.info(`${files.length} files changed`); 29 | 30 | // We load the pull request diff in order to access the contents of the new tweets from 31 | // pull requests coming from forks. The action does not have access to that git tree, 32 | // neither does the action’s token have access to the fork repository 33 | const { data } = await octokit.request( 34 | "GET /repos/:owner/:repo/pulls/:number", 35 | { 36 | headers: { 37 | accept: "application/vnd.github.diff", 38 | }, 39 | owner: payload.repository.owner.login, 40 | repo: payload.repository.name, 41 | number: payload.pull_request.number, 42 | } 43 | ); 44 | 45 | const newTweets = parseDiff(data) 46 | .filter((file) => file.new && /^tweets\/.*\.tweet$/.test(file.to)) 47 | .map((file) => 48 | file.chunks[0].changes 49 | .filter((line) => line.content.startsWith("+")) // ignore No newline at EOF 50 | .map((line) => line.content.substr(1)) 51 | .join("\n") 52 | ); 53 | 54 | toolkit.info(`New tweets found: ${newTweets.length}`); 55 | return newTweets; 56 | } 57 | -------------------------------------------------------------------------------- /lib/pull-request/index.js: -------------------------------------------------------------------------------- 1 | module.exports = handlePullRequest; 2 | 3 | const getNewTweets = require("./get-new-tweets"); 4 | const createCheckRun = require("./create-check-run"); 5 | 6 | async function handlePullRequest(state) { 7 | const { octokit, toolkit, payload } = state; 8 | 9 | // ignore builds from branches other than the repository’s defaul branch 10 | const base = payload.pull_request.base.ref; 11 | const defaultBranch = payload.repository.default_branch; 12 | if (defaultBranch !== base) { 13 | return toolkit.info( 14 | `Pull request base "${base}" is not the repository’s default branch` 15 | ); 16 | } 17 | 18 | // on request errors, log the requset options and error, then end process 19 | octokit.hook.error("request", (error) => { 20 | toolkit.info(error); 21 | toolkit.setFailed(error.stack); 22 | process.exit(); 23 | }); 24 | 25 | const newTweets = await getNewTweets(state); 26 | await createCheckRun(state, newTweets); 27 | } 28 | -------------------------------------------------------------------------------- /lib/push/add-comment.js: -------------------------------------------------------------------------------- 1 | module.exports = addComment; 2 | 3 | function addComment({ octokit, payload }, body) { 4 | // add comment with tweet URLs 5 | // https://developer.github.com/v3/repos/comments/#create-a-commit-comment 6 | return octokit.request("POST /repos/:owner/:repo/commits/:sha/comments", { 7 | owner: payload.repository.owner.login, 8 | repo: payload.repository.name, 9 | sha: payload.head_commit.id, 10 | body, 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /lib/push/get-new-tweets.js: -------------------------------------------------------------------------------- 1 | module.exports = getNewTweets; 2 | 3 | const { resolve: resolvePath } = require("path"); 4 | const { readFileSync } = require("fs"); 5 | 6 | async function getNewTweets({ payload, octokit }) { 7 | const { 8 | data: { files }, 9 | } = await octokit.request("GET /repos/:owner/:repo/compare/:base...:head", { 10 | owner: payload.repository.owner.login, 11 | repo: payload.repository.name, 12 | base: payload.before, 13 | head: payload.after, 14 | }); 15 | 16 | return files 17 | .filter( 18 | (file) => 19 | file.status === "added" && /^tweets\/.*\.tweet$/.test(file.filename) 20 | ) 21 | .map((file) => { 22 | const text = readFileSync( 23 | resolvePath(process.env.GITHUB_WORKSPACE, file.filename), 24 | "utf8" 25 | ).trim(); 26 | return { 27 | text, 28 | filename: file.filename, 29 | }; 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /lib/push/index.js: -------------------------------------------------------------------------------- 1 | module.exports = handlePush; 2 | 3 | const { inspect } = require("util"); 4 | 5 | const addComment = require("./add-comment"); 6 | const getNewTweets = require("./get-new-tweets"); 7 | const isSetupDone = require("./is-setup-done"); 8 | const setup = require("./setup"); 9 | const tweet = require("../common/tweet"); 10 | 11 | const parseTweetFileContent = require("../common/parse-tweet-file-content"); 12 | 13 | async function handlePush(state) { 14 | const { toolkit, octokit, payload, ref } = state; 15 | 16 | // ignore builds from tags 17 | if (!ref.startsWith("refs/heads/")) { 18 | toolkit.info(`GITHUB_REF is not a branch: ${ref}`); 19 | return; 20 | } 21 | 22 | // ignore builds from branches other than the repository’s defaul branch 23 | const defaultBranch = payload.repository.default_branch; 24 | const branch = process.env.GITHUB_REF.substr("refs/heads/".length); 25 | if (branch !== defaultBranch) { 26 | toolkit.info(`"${branch}" is not the default branch`); 27 | return; 28 | } 29 | 30 | // on request errors, log the requset options and error, then end process 31 | octokit.hook.error("request", (error, options) => { 32 | if (options.request.expectStatus === error.status) { 33 | throw error; 34 | } 35 | 36 | toolkit.info(error); 37 | toolkit.setFailed(error.stack); 38 | process.exit(); 39 | }); 40 | 41 | // make sure repository is already setup 42 | if (!(await isSetupDone())) { 43 | toolkit.info("tweets/ folder does not yet exist. Starting setup"); 44 | return setup(state); 45 | } 46 | 47 | // find tweets 48 | const newTweets = await getNewTweets(state); 49 | if (newTweets.length === 0) { 50 | toolkit.info("No new tweets"); 51 | return; 52 | } 53 | 54 | // post all the tweets 55 | const tweetUrls = []; 56 | const tweetErrors = []; 57 | for (let i = 0; i < newTweets.length; i++) { 58 | try { 59 | const parsed = parseTweetFileContent(newTweets[i].text, state.dir); 60 | 61 | toolkit.info(`Tweeting: ${parsed.text}`); 62 | if (parsed.poll) { 63 | toolkit.info( 64 | `Tweet has poll with ${ 65 | parsed.poll.length 66 | } options: ${parsed.poll.join(", ")}` 67 | ); 68 | } 69 | 70 | let result = await tweet(state, parsed, newTweets[i].filename); 71 | while (result) { 72 | toolkit.info(`tweeted: ${result.url}`); 73 | tweetUrls.push(result.url); 74 | result = result.thread; 75 | } 76 | } catch (error) { 77 | console.log(`error`); 78 | console.log(error[0] || error); 79 | tweetErrors.push(error[0] || error); 80 | } 81 | } 82 | 83 | if (tweetUrls.length) { 84 | await addComment(state, "Tweeted:\n\n- " + tweetUrls.join("\n- ")); 85 | } 86 | 87 | if (tweetErrors.length) { 88 | tweetErrors.forEach((error) => toolkit.error(inspect(error))); 89 | await addComment( 90 | state, 91 | "Errors:\n\n- " + tweetErrors.map((error) => error.message).join("\n- ") 92 | ); 93 | return toolkit.setFailed("Error tweeting"); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/push/is-setup-done.js: -------------------------------------------------------------------------------- 1 | module.exports = isSetupDone; 2 | 3 | const fs = require("fs"); 4 | const { resolve: resolvePath } = require("path"); 5 | 6 | function isSetupDone() { 7 | const tweetsFolderPath = resolvePath(process.env.GITHUB_WORKSPACE, "tweets"); 8 | return new Promise((resolve) => { 9 | fs.stat(tweetsFolderPath, (error, stat) => { 10 | if (error) { 11 | return resolve(false); 12 | } 13 | 14 | resolve(stat.isDirectory()); 15 | }); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /lib/push/setup.js: -------------------------------------------------------------------------------- 1 | module.exports = setup; 2 | 3 | async function setup({ toolkit, octokit, payload, sha }) { 4 | toolkit.info('Checking if "twitter-together-setup" branch exists already'); 5 | 6 | try { 7 | // Check if "twitter-together-setup" branch exists 8 | // https://developer.github.com/v3/git/refs/#get-a-reference 9 | await octokit.request("HEAD /repos/:owner/:repo/git/refs/:ref", { 10 | owner: payload.repository.owner.login, 11 | repo: payload.repository.name, 12 | ref: "heads/twitter-together-setup", 13 | request: { 14 | expectStatus: 404, 15 | }, 16 | }); 17 | 18 | // If it does, the script assumes that the setup pull requset already exists 19 | // and stops here 20 | return toolkit.info('"twitter-together-setup" branch already exists'); 21 | } catch (error) { 22 | toolkit.info('"twitter-together-setup" branch does not yet exist'); 23 | } 24 | 25 | // Create the "twitter-together-setup" branch 26 | // https://developer.github.com/v3/git/refs/#create-a-reference 27 | await octokit.request("POST /repos/:owner/:repo/git/refs", { 28 | owner: payload.repository.owner.login, 29 | repo: payload.repository.name, 30 | ref: "refs/heads/twitter-together-setup", 31 | sha, 32 | }); 33 | toolkit.info('"twitter-together-setup" branch created'); 34 | 35 | // Create tweets/README.md from same file in twitter-together/action repo 36 | // https://developer.github.com/v3/repos/contents/#get-contents 37 | const { data: readmeContent } = await octokit.request( 38 | "GET /repos/:owner/:repo/contents/:path", 39 | { 40 | mediaType: { 41 | format: "raw", 42 | }, 43 | owner: "twitter-together", 44 | repo: "action", 45 | path: "tweets/README.md", 46 | } 47 | ); 48 | // https://developer.github.com/v3/repos/contents/#create-or-update-a-file 49 | await octokit.request("PUT /repos/:owner/:repo/contents/:path", { 50 | owner: payload.repository.owner.login, 51 | repo: payload.repository.name, 52 | path: "tweets/README.md", 53 | content: Buffer.from(readmeContent).toString("base64"), 54 | branch: "twitter-together-setup", 55 | message: "twitter-together setup", 56 | }); 57 | toolkit.info('"tweets/README.md" created in "twitter-together-setup" branch'); 58 | 59 | // Create pull request 60 | // https://developer.github.com/v3/pulls/#create-a-pull-request 61 | const { data: pr } = await octokit.request("POST /repos/:owner/:repo/pulls", { 62 | owner: payload.repository.owner.login, 63 | repo: payload.repository.name, 64 | title: "🐦 twitter-together setup", 65 | body: `This pull request creates the \`tweets/\` folder where your \`*.tweet\` files go into. It also creates the \`tweets/README.md\` file with instructions. 66 | 67 | Note that if you plan to support tweets with polls, your app has to be approved for Twitter's Ads API. See [the Ads API Application Form](https://github.com/twitter-together/action/blob/main/docs/03-apply-for-access-to-the-twitter-ads-api.md) documentation for more details. 68 | 69 | Enjoy!`, 70 | head: "twitter-together-setup", 71 | base: payload.repository.default_branch, 72 | }); 73 | toolkit.info(`Setup pull request created: ${pr.html_url}`); 74 | } 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitter-together", 3 | "version": "3.1.0", 4 | "description": "A GitHub action to tweet together using pull requests", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "ncc build lib/index.js -o dist", 8 | "coverage": "nyc report --reporter=html && open coverage/index.html", 9 | "lint": "prettier --check '{lib,test}/**/*.js' 'docs/*.md' 'tweets/README.md' README.md package.json", 10 | "lint:fix": "prettier --write '{lib,test}/**/*.js' 'docs/*.md' 'tweets/README.md' README.md package.json", 11 | "test": "tap --branches=70 --functions=100 --lines=80 --statements=80 test/*/test.js", 12 | "posttest": "npm run -s lint" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "MIT", 17 | "dependencies": { 18 | "@actions/core": "1.9.1", 19 | "@octokit/action": "^4.0.8", 20 | "js-yaml": "^4.1.0", 21 | "mime-types": "^2.1.35", 22 | "parse-diff": "^0.9.0", 23 | "twitter-api-v2": "^1.12.7", 24 | "twitter-text": "^3.1.0" 25 | }, 26 | "devDependencies": { 27 | "@vercel/ncc": "^0.34.0", 28 | "nock": "^13.2.9", 29 | "prettier": "^2.7.1", 30 | "semantic-release": "^19.0.5", 31 | "tap": "^16.3.0" 32 | }, 33 | "repository": "github:twitter-together/action", 34 | "release": { 35 | "branches": [ 36 | "+([0-9]).x", 37 | "main", 38 | "next", 39 | { 40 | "name": "beta", 41 | "prerelease": true 42 | }, 43 | { 44 | "name": "debug", 45 | "prerelease": true 46 | } 47 | ], 48 | "plugins": [ 49 | "@semantic-release/commit-analyzer", 50 | "@semantic-release/release-notes-generator", 51 | "@semantic-release/github", 52 | [ 53 | "@semantic-release/npm", 54 | { 55 | "npmPublish": false 56 | } 57 | ], 58 | [ 59 | "@semantic-release/exec", 60 | { 61 | "publishCmd": "npm run build" 62 | } 63 | ], 64 | [ 65 | "@semantic-release/git", 66 | { 67 | "assets": [ 68 | "package.json", 69 | "dist/index.js" 70 | ], 71 | "message": "build(release): compiled action for ${nextRelease.version}\n\n[skip ci]" 72 | } 73 | ] 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/command-line-has-invalid-tweet/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the ability to invoke a tweet using the `--file` command line flag 3 | * but provides a missing filename so generates an error. 4 | */ 5 | 6 | const nock = require("nock"); 7 | const tap = require("tap"); 8 | 9 | // MOCK 10 | process.chdir(__dirname); 11 | process.argv = [ 12 | "node", 13 | "lib/index.js", 14 | "--file", 15 | "tweets/does-not-exist.tweet", 16 | ]; 17 | 18 | process.on("exit", (code) => { 19 | tap.equal(code, 1); 20 | tap.same(nock.pendingMocks(), []); 21 | 22 | // above code exits with 1 (error), but tap expects 0. 23 | // Tap adds the "process.exitCode" property for that purpose. 24 | process.exitCode = 0; 25 | }); 26 | 27 | require("../../lib"); 28 | -------------------------------------------------------------------------------- /test/command-line-has-no-tweet/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the ability to invoke a tweet using the `--file` command line flag 3 | * but provides no actual filename so generates an error. 4 | */ 5 | 6 | const nock = require("nock"); 7 | const tap = require("tap"); 8 | 9 | // MOCK 10 | process.chdir(__dirname); 11 | process.argv = ["node", "lib/index.js", "--file"]; 12 | 13 | process.on("exit", (code) => { 14 | tap.equal(code, 1); 15 | tap.same(nock.pendingMocks(), []); 16 | 17 | // above code exits with 1 (error), but tap expects 0. 18 | // Tap adds the "process.exitCode" property for that purpose. 19 | process.exitCode = 0; 20 | }); 21 | 22 | require("../../lib"); 23 | -------------------------------------------------------------------------------- /test/command-line-has-tweet-with-front-matter-media-in-directory/custom/media/blahaj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twitter-together/action/08857009da2aacd9bd08204550ec96e15d76b4da/test/command-line-has-tweet-with-front-matter-media-in-directory/custom/media/blahaj.png -------------------------------------------------------------------------------- /test/command-line-has-tweet-with-front-matter-media-in-directory/custom/tweets/hello-world.tweet: -------------------------------------------------------------------------------- 1 | --- 2 | media: 3 | - file: blahaj.png 4 | alt: Blahaj! 5 | --- 6 | 7 | Cuddly :) 8 | -------------------------------------------------------------------------------- /test/command-line-has-tweet-with-front-matter-media-in-directory/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the ability to invoke a tweet using the `--file` command line flag 3 | * which is in a custom directory and has front matter that includes media. 4 | */ 5 | 6 | const nock = require("nock"); 7 | const tap = require("tap"); 8 | 9 | // SETUP 10 | process.env.TWITTER_API_KEY = "key123"; 11 | process.env.TWITTER_API_SECRET_KEY = "keysecret123"; 12 | process.env.TWITTER_ACCESS_TOKEN = "token123"; 13 | process.env.TWITTER_ACCESS_TOKEN_SECRET = "tokensecret123"; 14 | 15 | // MOCK 16 | nock("https://api.twitter.com") 17 | .get("/2/users/me") 18 | .reply(200, { 19 | data: { 20 | id: "123", 21 | name: "gr2m", 22 | username: "gr2m", 23 | }, 24 | }) 25 | 26 | .post("/2/tweets", (body) => { 27 | tap.equal(body.text, "Cuddly :)"); 28 | tap.type(body.media, "object"); 29 | tap.hasProp(body.media, "media_ids"); 30 | tap.same(body.media.media_ids, ["0000000000000000002"]); 31 | return true; 32 | }) 33 | .reply(201, { 34 | data: { 35 | id: "0000000000000000001", 36 | text: "Cuddly :) https://t.co/abcdeFGHIJ", 37 | }, 38 | }); 39 | 40 | nock("https://upload.twitter.com") 41 | .post("/1.1/media/upload.json", (body) => { 42 | tap.match( 43 | body, 44 | 'Content-Disposition: form-data; name="command"\r\n\r\nINIT' 45 | ); 46 | tap.match( 47 | body, 48 | 'Content-Disposition: form-data; name="total_bytes"\r\n\r\n107352' 49 | ); 50 | tap.match( 51 | body, 52 | 'Content-Disposition: form-data; name="media_type"\r\n\r\nimage/png' 53 | ); 54 | return true; 55 | }) 56 | .reply(202, { 57 | media_id_string: "0000000000000000002", 58 | expires_after_secs: 86400, 59 | media_key: "3_0000000000000000002", 60 | }) 61 | 62 | .post("/1.1/media/upload.json") 63 | .reply(204) 64 | 65 | .post("/1.1/media/upload.json", (body) => { 66 | tap.match( 67 | body, 68 | 'Content-Disposition: form-data; name="command"\r\n\r\nFINALIZE' 69 | ); 70 | tap.match( 71 | body, 72 | 'Content-Disposition: form-data; name="media_id"\r\n\r\n0000000000000000002' 73 | ); 74 | return true; 75 | }) 76 | .reply(201, { 77 | media_id_string: "0000000000000000002", 78 | media_key: "3_0000000000000000002", 79 | size: 107352, 80 | expires_after_secs: 86400, 81 | image: { 82 | image_type: "image/png", 83 | w: 640, 84 | h: 360, 85 | }, 86 | }) 87 | 88 | .post("/1.1/media/metadata/create.json", (body) => { 89 | tap.equal(body.media_id, "0000000000000000002"); 90 | tap.type(body.alt_text, "object"); 91 | tap.hasProp(body.alt_text, "text"); 92 | tap.equal(body.alt_text.text, "Blahaj!"); 93 | return true; 94 | }) 95 | .reply(200); 96 | 97 | process.chdir(__dirname); 98 | process.argv = [ 99 | "node", 100 | "lib/index.js", 101 | "--file", 102 | "custom/tweets/hello-world.tweet", 103 | ]; 104 | 105 | process.on("exit", (code) => { 106 | tap.equal(code, 0); 107 | tap.same(nock.pendingMocks(), []); 108 | }); 109 | 110 | require("../../lib"); 111 | -------------------------------------------------------------------------------- /test/command-line-has-tweet-with-front-matter-media/media/blahaj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twitter-together/action/08857009da2aacd9bd08204550ec96e15d76b4da/test/command-line-has-tweet-with-front-matter-media/media/blahaj.png -------------------------------------------------------------------------------- /test/command-line-has-tweet-with-front-matter-media/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the ability to invoke a tweet using the `--file` command line flag 3 | * which has front matter that includes media. 4 | */ 5 | 6 | const nock = require("nock"); 7 | const tap = require("tap"); 8 | 9 | // SETUP 10 | process.env.TWITTER_API_KEY = "key123"; 11 | process.env.TWITTER_API_SECRET_KEY = "keysecret123"; 12 | process.env.TWITTER_ACCESS_TOKEN = "token123"; 13 | process.env.TWITTER_ACCESS_TOKEN_SECRET = "tokensecret123"; 14 | 15 | // MOCK 16 | nock("https://api.twitter.com") 17 | .get("/2/users/me") 18 | .reply(200, { 19 | data: { 20 | id: "123", 21 | name: "gr2m", 22 | username: "gr2m", 23 | }, 24 | }) 25 | 26 | .post("/2/tweets", (body) => { 27 | tap.equal(body.text, "Cuddly :)"); 28 | tap.type(body.media, "object"); 29 | tap.hasProp(body.media, "media_ids"); 30 | tap.same(body.media.media_ids, ["0000000000000000002"]); 31 | return true; 32 | }) 33 | .reply(201, { 34 | data: { 35 | id: "0000000000000000001", 36 | text: "Cuddly :) https://t.co/abcdeFGHIJ", 37 | }, 38 | }); 39 | 40 | nock("https://upload.twitter.com") 41 | .post("/1.1/media/upload.json", (body) => { 42 | tap.match( 43 | body, 44 | 'Content-Disposition: form-data; name="command"\r\n\r\nINIT' 45 | ); 46 | tap.match( 47 | body, 48 | 'Content-Disposition: form-data; name="total_bytes"\r\n\r\n107352' 49 | ); 50 | tap.match( 51 | body, 52 | 'Content-Disposition: form-data; name="media_type"\r\n\r\nimage/png' 53 | ); 54 | return true; 55 | }) 56 | .reply(202, { 57 | media_id_string: "0000000000000000002", 58 | expires_after_secs: 86400, 59 | media_key: "3_0000000000000000002", 60 | }) 61 | 62 | .post("/1.1/media/upload.json") 63 | .reply(204) 64 | 65 | .post("/1.1/media/upload.json", (body) => { 66 | tap.match( 67 | body, 68 | 'Content-Disposition: form-data; name="command"\r\n\r\nFINALIZE' 69 | ); 70 | tap.match( 71 | body, 72 | 'Content-Disposition: form-data; name="media_id"\r\n\r\n0000000000000000002' 73 | ); 74 | return true; 75 | }) 76 | .reply(201, { 77 | media_id_string: "0000000000000000002", 78 | media_key: "3_0000000000000000002", 79 | size: 107352, 80 | expires_after_secs: 86400, 81 | image: { 82 | image_type: "image/png", 83 | w: 640, 84 | h: 360, 85 | }, 86 | }) 87 | 88 | .post("/1.1/media/metadata/create.json", (body) => { 89 | tap.equal(body.media_id, "0000000000000000002"); 90 | tap.type(body.alt_text, "object"); 91 | tap.hasProp(body.alt_text, "text"); 92 | tap.equal(body.alt_text.text, "Blahaj!"); 93 | return true; 94 | }) 95 | .reply(200); 96 | 97 | process.chdir(__dirname); 98 | process.argv = ["node", "lib/index.js", "--file", "tweets/hello-world.tweet"]; 99 | 100 | process.on("exit", (code) => { 101 | tap.equal(code, 0); 102 | tap.same(nock.pendingMocks(), []); 103 | }); 104 | 105 | require("../../lib"); 106 | -------------------------------------------------------------------------------- /test/command-line-has-tweet-with-front-matter-media/tweets/hello-world.tweet: -------------------------------------------------------------------------------- 1 | --- 2 | media: 3 | - file: blahaj.png 4 | alt: Blahaj! 5 | --- 6 | 7 | Cuddly :) 8 | -------------------------------------------------------------------------------- /test/command-line-has-tweet/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the ability to invoke a tweet using the `--file` command line flag. 3 | */ 4 | 5 | const nock = require("nock"); 6 | const tap = require("tap"); 7 | 8 | // SETUP 9 | process.env.TWITTER_API_KEY = "key123"; 10 | process.env.TWITTER_API_SECRET_KEY = "keysecret123"; 11 | process.env.TWITTER_ACCESS_TOKEN = "token123"; 12 | process.env.TWITTER_ACCESS_TOKEN_SECRET = "tokensecret123"; 13 | 14 | // MOCK 15 | nock("https://api.twitter.com") 16 | .get("/2/users/me") 17 | .reply(200, { 18 | data: { 19 | id: "123", 20 | name: "gr2m", 21 | username: "gr2m", 22 | }, 23 | }) 24 | 25 | .post("/2/tweets", (body) => { 26 | tap.equal(body.text, "Hello, world!"); 27 | return true; 28 | }) 29 | .reply(201, { 30 | data: { 31 | id: "0000000000000000001", 32 | text: "Hello, world!", 33 | }, 34 | }); 35 | 36 | process.chdir(__dirname); 37 | process.argv = ["node", "lib/index.js", "--file", "tweets/hello-world.tweet"]; 38 | 39 | process.on("exit", (code) => { 40 | tap.equal(code, 0); 41 | tap.same(nock.pendingMocks(), []); 42 | }); 43 | 44 | require("../../lib"); 45 | -------------------------------------------------------------------------------- /test/command-line-has-tweet/tweets/hello-world.tweet: -------------------------------------------------------------------------------- 1 | Hello, world! 2 | -------------------------------------------------------------------------------- /test/pull-request-from-fork-has-tweet/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "opened", 3 | "pull_request": { 4 | "number": 123, 5 | "base": { 6 | "ref": "main" 7 | }, 8 | "head": { 9 | "sha": "0000000000000000000000000000000000000002", 10 | "repo": { 11 | "fork": true 12 | } 13 | } 14 | }, 15 | "repository": { 16 | "default_branch": "main", 17 | "name": "action", 18 | "owner": { 19 | "login": "twitter-together" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/pull-request-from-fork-has-tweet/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of pull request adding a new *.tweet file 3 | */ 4 | 5 | const nock = require("nock"); 6 | const tap = require("tap"); 7 | 8 | // SETUP 9 | process.env.GITHUB_EVENT_NAME = "pull_request"; 10 | process.env.GITHUB_TOKEN = "secret123"; 11 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 12 | 13 | // set other env variables so action-toolkit is happy 14 | process.env.GITHUB_REF = ""; 15 | process.env.GITHUB_WORKSPACE = ""; 16 | process.env.GITHUB_WORKFLOW = ""; 17 | process.env.GITHUB_ACTION = "twitter-together"; 18 | process.env.GITHUB_ACTOR = ""; 19 | process.env.GITHUB_REPOSITORY = ""; 20 | process.env.GITHUB_SHA = ""; 21 | 22 | // MOCK 23 | nock("https://api.github.com", { 24 | reqheaders: { 25 | authorization: "token secret123", 26 | }, 27 | }) 28 | // get changed files 29 | .get("/repos/twitter-together/action/pulls/123/files") 30 | .reply(200, [ 31 | { 32 | status: "added", 33 | filename: "tweets/hello-world.tweet", 34 | }, 35 | ]); 36 | 37 | // get pull request diff 38 | nock("https://api.github.com", { 39 | reqheaders: { 40 | accept: "application/vnd.github.diff", 41 | authorization: "token secret123", 42 | }, 43 | }) 44 | .get("/repos/twitter-together/action/pulls/123") 45 | .reply( 46 | 200, 47 | `diff --git a/tweets/hello-world.tweet b/tweets/hello-world.tweet 48 | new file mode 100644 49 | index 0000000..0123456 50 | --- /dev/null 51 | +++ b/tweets/hello-world.tweet 52 | @@ -0,0 +1 @@ 53 | +Hello, world!` 54 | ); 55 | 56 | process.on("exit", (code) => { 57 | tap.equal(code, 0); 58 | tap.same(nock.pendingMocks(), []); 59 | }); 60 | 61 | require("../../lib"); 62 | -------------------------------------------------------------------------------- /test/pull-request-from-fork-invalid-tweet/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "opened", 3 | "pull_request": { 4 | "number": 123, 5 | "base": { 6 | "ref": "main" 7 | }, 8 | "head": { 9 | "sha": "0000000000000000000000000000000000000002", 10 | "repo": { 11 | "fork": true 12 | } 13 | } 14 | }, 15 | "repository": { 16 | "default_branch": "main", 17 | "name": "action", 18 | "owner": { 19 | "login": "twitter-together" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/pull-request-from-fork-invalid-tweet/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of pull request adding a new *.tweet file 3 | */ 4 | 5 | const nock = require("nock"); 6 | const tap = require("tap"); 7 | 8 | // SETUP 9 | process.env.GITHUB_EVENT_NAME = "pull_request"; 10 | process.env.GITHUB_TOKEN = "secret123"; 11 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 12 | 13 | // set other env variables so action-toolkit is happy 14 | process.env.GITHUB_REF = ""; 15 | process.env.GITHUB_WORKSPACE = ""; 16 | process.env.GITHUB_WORKFLOW = ""; 17 | process.env.GITHUB_ACTION = "twitter-together"; 18 | process.env.GITHUB_ACTOR = ""; 19 | process.env.GITHUB_REPOSITORY = ""; 20 | process.env.GITHUB_SHA = ""; 21 | 22 | // MOCK 23 | nock("https://api.github.com", { 24 | reqheaders: { 25 | authorization: "token secret123", 26 | }, 27 | }) 28 | // get changed files 29 | .get("/repos/twitter-together/action/pulls/123/files") 30 | .reply(200, [ 31 | { 32 | status: "added", 33 | filename: "tweets/hello-world.tweet", 34 | }, 35 | ]); 36 | 37 | // get pull request diff 38 | nock("https://api.github.com", { 39 | reqheaders: { 40 | accept: "application/vnd.github.diff", 41 | authorization: "token secret123", 42 | }, 43 | }) 44 | .get("/repos/twitter-together/action/pulls/123") 45 | .reply( 46 | 200, 47 | `diff --git a/tweets/hello-world.tweet b/tweets/hello-world.tweet 48 | new file mode 100644 49 | index 0000000..0123456 50 | --- /dev/null 51 | +++ b/tweets/hello-world.tweet 52 | @@ -0,0 +1 @@ 53 | +Cupcake ipsum dolor sit amet chupa chups candy halvah I love. Apple pie gummi bears chupa chups jujubes I love cake jelly. Jelly candy canes pudding jujubes caramels sweet roll I love. Sweet fruitcake oat cake I love brownie sesame snaps apple pie lollipop. Pie dragée I love apple pie cotton candy candy chocolate bar.` 54 | ); 55 | 56 | process.on("exit", (code) => { 57 | tap.equal(code, 1); 58 | tap.same(nock.pendingMocks(), []); 59 | 60 | // above code exits with 1 (error), but tap expects 0. 61 | // Tap adds the "process.exitCode" property for that purpose. 62 | process.exitCode = 0; 63 | }); 64 | 65 | require("../../lib"); 66 | -------------------------------------------------------------------------------- /test/pull-request-has-tweet-no-newline/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "opened", 3 | "pull_request": { 4 | "number": 123, 5 | "base": { 6 | "ref": "main" 7 | }, 8 | "head": { 9 | "sha": "0000000000000000000000000000000000000002", 10 | "repo": { 11 | "fork": false 12 | } 13 | } 14 | }, 15 | "repository": { 16 | "default_branch": "main", 17 | "name": "action", 18 | "owner": { 19 | "login": "twitter-together" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/pull-request-has-tweet-no-newline/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of pull request adding a new *.tweet file 3 | */ 4 | 5 | const nock = require("nock"); 6 | const tap = require("tap"); 7 | 8 | // SETUP 9 | process.env.GITHUB_EVENT_NAME = "pull_request"; 10 | process.env.GITHUB_TOKEN = "secret123"; 11 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 12 | 13 | // set other env variables so action-toolkit is happy 14 | process.env.GITHUB_REF = ""; 15 | process.env.GITHUB_WORKSPACE = ""; 16 | process.env.GITHUB_WORKFLOW = ""; 17 | process.env.GITHUB_ACTION = "twitter-together"; 18 | process.env.GITHUB_ACTOR = ""; 19 | process.env.GITHUB_REPOSITORY = ""; 20 | process.env.GITHUB_SHA = ""; 21 | 22 | // MOCK 23 | nock("https://api.github.com", { 24 | reqheaders: { 25 | authorization: "token secret123", 26 | }, 27 | }) 28 | // get changed files 29 | .get("/repos/twitter-together/action/pulls/123/files") 30 | .reply(200, [ 31 | { 32 | status: "added", 33 | filename: "tweets/hello-world.tweet", 34 | }, 35 | ]); 36 | 37 | // get pull request diff 38 | nock("https://api.github.com", { 39 | reqheaders: { 40 | accept: "application/vnd.github.diff", 41 | authorization: "token secret123", 42 | }, 43 | }) 44 | .get("/repos/twitter-together/action/pulls/123") 45 | .reply( 46 | 200, 47 | `diff --git a/tweets/hello-world.tweet b/tweets/hello-world.tweet 48 | new file mode 100644 49 | index 0000000..0123456 50 | --- /dev/null 51 | +++ b/tweets/hello-world.tweet 52 | @@ -0,0 +1 @@ 53 | +Hello, world! 54 | \\ No newline at end of file 55 | ` 56 | ); 57 | 58 | // create check run 59 | nock("https://api.github.com") 60 | // get changed files 61 | .post("/repos/twitter-together/action/check-runs", (body) => { 62 | tap.equal(body.name, "preview"); 63 | tap.equal(body.head_sha, "0000000000000000000000000000000000000002"); 64 | tap.equal(body.status, "completed"); 65 | tap.equal(body.conclusion, "success"); 66 | tap.same(body.output, { 67 | title: "1 tweet(s)", 68 | summary: "### ✅ Valid\n\n> Hello, world!", 69 | }); 70 | 71 | return true; 72 | }) 73 | .reply(201); 74 | 75 | process.on("exit", (code) => { 76 | tap.equal(code, 0); 77 | tap.same(nock.pendingMocks(), []); 78 | }); 79 | 80 | require("../../lib"); 81 | -------------------------------------------------------------------------------- /test/pull-request-has-tweet-with-poll-with-5-options/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "opened", 3 | "pull_request": { 4 | "number": 123, 5 | "base": { 6 | "ref": "main" 7 | }, 8 | "head": { 9 | "sha": "0000000000000000000000000000000000000002", 10 | "repo": { 11 | "fork": false 12 | } 13 | } 14 | }, 15 | "repository": { 16 | "default_branch": "main", 17 | "name": "action", 18 | "owner": { 19 | "login": "twitter-together" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/pull-request-has-tweet-with-poll-with-5-options/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of pull request adding a new *.tweet file 3 | */ 4 | 5 | const nock = require("nock"); 6 | const tap = require("tap"); 7 | 8 | // SETUP 9 | process.env.GITHUB_EVENT_NAME = "pull_request"; 10 | process.env.GITHUB_TOKEN = "secret123"; 11 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 12 | 13 | // set other env variables so action-toolkit is happy 14 | process.env.GITHUB_REF = ""; 15 | process.env.GITHUB_WORKSPACE = ""; 16 | process.env.GITHUB_WORKFLOW = ""; 17 | process.env.GITHUB_ACTION = "twitter-together"; 18 | process.env.GITHUB_ACTOR = ""; 19 | process.env.GITHUB_REPOSITORY = ""; 20 | process.env.GITHUB_SHA = ""; 21 | 22 | // MOCK 23 | nock("https://api.github.com", { 24 | reqheaders: { 25 | authorization: "token secret123", 26 | }, 27 | }) 28 | // get changed files 29 | .get("/repos/twitter-together/action/pulls/123/files") 30 | .reply(200, [ 31 | { 32 | status: "added", 33 | filename: "tweets/hello-world.tweet", 34 | }, 35 | ]); 36 | 37 | // get pull request diff 38 | nock("https://api.github.com", { 39 | reqheaders: { 40 | accept: "application/vnd.github.diff", 41 | authorization: "token secret123", 42 | }, 43 | }) 44 | .get("/repos/twitter-together/action/pulls/123") 45 | .reply( 46 | 200, 47 | `diff --git a/tweets/hello-world.tweet b/tweets/hello-world.tweet 48 | new file mode 100644 49 | index 0000000..0123456 50 | --- /dev/null 51 | +++ b/tweets/hello-world.tweet 52 | @@ -0,0 +1,7 @@ 53 | +Here is my poll 54 | + 55 | +( ) option 1 56 | +( ) option 2 57 | +( ) option 3 58 | +( ) option 4 59 | +( ) option 5` 60 | ); 61 | 62 | // create check run 63 | nock("https://api.github.com") 64 | // get changed files 65 | .post("/repos/twitter-together/action/check-runs", (body) => { 66 | tap.equal(body.name, "preview"); 67 | tap.equal(body.head_sha, "0000000000000000000000000000000000000002"); 68 | tap.equal(body.status, "completed"); 69 | tap.equal(body.conclusion, "failure"); 70 | tap.same(body.output, { 71 | title: "1 tweet(s)", 72 | summary: `### ❌ Invalid 73 | 74 | > Here is my poll 75 | > 76 | > ( ) option 1 77 | > ( ) option 2 78 | > ( ) option 3 79 | > ( ) option 4 80 | > ( ) option 5 81 | 82 | Polls cannot have more than four options, found 5 options`, 83 | }); 84 | 85 | return true; 86 | }) 87 | .reply(201); 88 | 89 | process.on("exit", (code) => { 90 | tap.equal(code, 0); 91 | tap.same(nock.pendingMocks(), []); 92 | }); 93 | 94 | require("../../lib"); 95 | -------------------------------------------------------------------------------- /test/pull-request-has-tweet-with-poll/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "opened", 3 | "pull_request": { 4 | "number": 123, 5 | "base": { 6 | "ref": "main" 7 | }, 8 | "head": { 9 | "sha": "0000000000000000000000000000000000000002", 10 | "repo": { 11 | "fork": false 12 | } 13 | } 14 | }, 15 | "repository": { 16 | "default_branch": "main", 17 | "name": "action", 18 | "owner": { 19 | "login": "twitter-together" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/pull-request-has-tweet-with-poll/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of pull request adding a new *.tweet file 3 | */ 4 | 5 | const nock = require("nock"); 6 | const tap = require("tap"); 7 | 8 | // SETUP 9 | process.env.GITHUB_EVENT_NAME = "pull_request"; 10 | process.env.GITHUB_TOKEN = "secret123"; 11 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 12 | 13 | // set other env variables so action-toolkit is happy 14 | process.env.GITHUB_REF = ""; 15 | process.env.GITHUB_WORKSPACE = ""; 16 | process.env.GITHUB_WORKFLOW = ""; 17 | process.env.GITHUB_ACTION = "twitter-together"; 18 | process.env.GITHUB_ACTOR = ""; 19 | process.env.GITHUB_REPOSITORY = ""; 20 | process.env.GITHUB_SHA = ""; 21 | 22 | // MOCK 23 | nock("https://api.github.com", { 24 | reqheaders: { 25 | authorization: "token secret123", 26 | }, 27 | }) 28 | // get changed files 29 | .get("/repos/twitter-together/action/pulls/123/files") 30 | .reply(200, [ 31 | { 32 | status: "added", 33 | filename: "tweets/hello-world.tweet", 34 | }, 35 | ]); 36 | 37 | // get pull request diff 38 | nock("https://api.github.com", { 39 | reqheaders: { 40 | accept: "application/vnd.github.diff", 41 | authorization: "token secret123", 42 | }, 43 | }) 44 | .get("/repos/twitter-together/action/pulls/123") 45 | .reply( 46 | 200, 47 | `diff --git a/tweets/hello-world.tweet b/tweets/hello-world.tweet 48 | new file mode 100644 49 | index 0000000..0123456 50 | --- /dev/null 51 | +++ b/tweets/hello-world.tweet 52 | @@ -0,0 +1,6 @@ 53 | +Here is my poll 54 | + 55 | +( ) option 1 56 | +() option 2 57 | +( ) option 3 58 | +() option 4` 59 | ); 60 | 61 | // create check run 62 | nock("https://api.github.com") 63 | // get changed files 64 | .post("/repos/twitter-together/action/check-runs", (body) => { 65 | tap.equal(body.name, "preview"); 66 | tap.equal(body.head_sha, "0000000000000000000000000000000000000002"); 67 | tap.equal(body.status, "completed"); 68 | tap.equal(body.conclusion, "success"); 69 | tap.same(body.output, { 70 | title: "1 tweet(s)", 71 | summary: `### ✅ Valid 72 | 73 | > Here is my poll 74 | 75 | The tweet includes a poll: 76 | 77 | > 🔘 option 1 78 | > 🔘 option 2 79 | > 🔘 option 3 80 | > 🔘 option 4`, 81 | }); 82 | 83 | return true; 84 | }) 85 | .reply(201); 86 | 87 | process.on("exit", (code) => { 88 | tap.equal(code, 0); 89 | tap.same(nock.pendingMocks(), []); 90 | }); 91 | 92 | require("../../lib"); 93 | -------------------------------------------------------------------------------- /test/pull-request-has-tweet/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "opened", 3 | "pull_request": { 4 | "number": 123, 5 | "base": { 6 | "ref": "main" 7 | }, 8 | "head": { 9 | "sha": "0000000000000000000000000000000000000002", 10 | "repo": { 11 | "fork": false 12 | } 13 | } 14 | }, 15 | "repository": { 16 | "default_branch": "main", 17 | "name": "action", 18 | "owner": { 19 | "login": "twitter-together" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/pull-request-has-tweet/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of pull request adding a new *.tweet file 3 | */ 4 | 5 | const nock = require("nock"); 6 | const tap = require("tap"); 7 | 8 | // SETUP 9 | process.env.GITHUB_EVENT_NAME = "pull_request"; 10 | process.env.GITHUB_TOKEN = "secret123"; 11 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 12 | 13 | // set other env variables so action-toolkit is happy 14 | process.env.GITHUB_REF = ""; 15 | process.env.GITHUB_WORKSPACE = ""; 16 | process.env.GITHUB_WORKFLOW = ""; 17 | process.env.GITHUB_ACTION = "twitter-together"; 18 | process.env.GITHUB_ACTOR = ""; 19 | process.env.GITHUB_REPOSITORY = ""; 20 | process.env.GITHUB_SHA = ""; 21 | 22 | // MOCK 23 | nock("https://api.github.com", { 24 | reqheaders: { 25 | authorization: "token secret123", 26 | }, 27 | }) 28 | // get changed files 29 | .get("/repos/twitter-together/action/pulls/123/files") 30 | .reply(200, [ 31 | { 32 | status: "added", 33 | filename: "tweets/hello-world.tweet", 34 | }, 35 | ]); 36 | 37 | // get pull request diff 38 | nock("https://api.github.com", { 39 | reqheaders: { 40 | accept: "application/vnd.github.diff", 41 | authorization: "token secret123", 42 | }, 43 | }) 44 | .get("/repos/twitter-together/action/pulls/123") 45 | .reply( 46 | 200, 47 | `diff --git a/tweets/hello-world.tweet b/tweets/hello-world.tweet 48 | new file mode 100644 49 | index 0000000..0123456 50 | --- /dev/null 51 | +++ b/tweets/hello-world.tweet 52 | @@ -0,0 +1 @@ 53 | +Hello, world!` 54 | ); 55 | 56 | // create check run 57 | nock("https://api.github.com") 58 | // get changed files 59 | .post("/repos/twitter-together/action/check-runs", (body) => { 60 | tap.equal(body.name, "preview"); 61 | tap.equal(body.head_sha, "0000000000000000000000000000000000000002"); 62 | tap.equal(body.status, "completed"); 63 | tap.equal(body.conclusion, "success"); 64 | tap.same(body.output, { 65 | title: "1 tweet(s)", 66 | summary: "### ✅ Valid\n\n> Hello, world!", 67 | }); 68 | 69 | return true; 70 | }) 71 | .reply(201); 72 | 73 | process.on("exit", (code) => { 74 | tap.equal(code, 0); 75 | tap.same(nock.pendingMocks(), []); 76 | }); 77 | 78 | require("../../lib"); 79 | -------------------------------------------------------------------------------- /test/pull-request-invalid-tweet/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "opened", 3 | "pull_request": { 4 | "number": 123, 5 | "base": { 6 | "ref": "main" 7 | }, 8 | "head": { 9 | "sha": "0000000000000000000000000000000000000002", 10 | "repo": { 11 | "fork": false 12 | } 13 | } 14 | }, 15 | "repository": { 16 | "default_branch": "main", 17 | "name": "action", 18 | "owner": { 19 | "login": "twitter-together" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/pull-request-invalid-tweet/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of pull request adding a new *.tweet file 3 | */ 4 | 5 | const nock = require("nock"); 6 | const tap = require("tap"); 7 | 8 | // SETUP 9 | process.env.GITHUB_EVENT_NAME = "pull_request"; 10 | process.env.GITHUB_TOKEN = "secret123"; 11 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 12 | 13 | // set other env variables so action-toolkit is happy 14 | process.env.GITHUB_REF = ""; 15 | process.env.GITHUB_WORKSPACE = ""; 16 | process.env.GITHUB_WORKFLOW = ""; 17 | process.env.GITHUB_ACTION = "twitter-together"; 18 | process.env.GITHUB_ACTOR = ""; 19 | process.env.GITHUB_REPOSITORY = ""; 20 | process.env.GITHUB_SHA = ""; 21 | 22 | // MOCK 23 | nock("https://api.github.com", { 24 | reqheaders: { 25 | authorization: "token secret123", 26 | }, 27 | }) 28 | // get changed files 29 | .get("/repos/twitter-together/action/pulls/123/files") 30 | .reply(200, [ 31 | { 32 | status: "added", 33 | filename: "tweets/hello-world.tweet", 34 | }, 35 | ]); 36 | 37 | // get pull request diff 38 | nock("https://api.github.com", { 39 | reqheaders: { 40 | accept: "application/vnd.github.diff", 41 | authorization: "token secret123", 42 | }, 43 | }) 44 | .get("/repos/twitter-together/action/pulls/123") 45 | .reply( 46 | 200, 47 | `diff --git a/tweets/hello-world.tweet b/tweets/hello-world.tweet 48 | new file mode 100644 49 | index 0000000..0123456 50 | --- /dev/null 51 | +++ b/tweets/hello-world.tweet 52 | @@ -0,0 +1 @@ 53 | +Cupcake ipsum dolor sit amet chupa chups candy halvah I love. Apple pie gummi bears chupa chups jujubes I love cake jelly. Jelly candy canes pudding jujubes caramels sweet roll I love. Sweet fruitcake oat cake I love brownie sesame snaps apple pie lollipop. Pie dragée I love apple pie cotton candy candy chocolate bar.` 54 | ); 55 | 56 | // create check run 57 | nock("https://api.github.com", { 58 | reqheaders: { 59 | authorization: "token secret123", 60 | }, 61 | }) 62 | .post("/repos/twitter-together/action/check-runs", (body) => { 63 | tap.equal(body.name, "preview"); 64 | tap.equal(body.head_sha, "0000000000000000000000000000000000000002"); 65 | tap.equal(body.status, "completed"); 66 | tap.equal(body.conclusion, "failure"); 67 | tap.same(body.output, { 68 | title: "1 tweet(s)", 69 | summary: `### ❌ Invalid 70 | 71 | > Cupcake ipsum dolor sit amet chupa chups candy halvah I love. Apple pie gummi bears chupa chups jujubes I love cake jelly. Jelly candy canes pudding jujubes caramels sweet roll I love. Sweet fruitcake oat cake I love brownie sesame snaps apple pie lollipop. Pie dragée I love apple pie cotton candy candy chocolate bar. 72 | 73 | Tweet exceeds maximum length of 280 characters by 39 characters`, 74 | }); 75 | 76 | return true; 77 | }) 78 | .reply(201); 79 | 80 | process.on("exit", (code) => { 81 | tap.equal(code, 0); 82 | tap.same(nock.pendingMocks(), []); 83 | }); 84 | 85 | require("../../lib"); 86 | -------------------------------------------------------------------------------- /test/pull-request-no-tweets/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "opened", 3 | "pull_request": { 4 | "number": 123, 5 | "base": { 6 | "ref": "main" 7 | }, 8 | "head": { 9 | "sha": "0000000000000000000000000000000000000002", 10 | "repo": { 11 | "fork": false 12 | } 13 | } 14 | }, 15 | "repository": { 16 | "default_branch": "main", 17 | "name": "action", 18 | "owner": { 19 | "login": "twitter-together" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/pull-request-no-tweets/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of pull request adding a new *.tweet file 3 | */ 4 | 5 | const tap = require("tap"); 6 | const nock = require("nock"); 7 | 8 | // SETUP 9 | process.env.GITHUB_EVENT_NAME = "pull_request"; 10 | process.env.GITHUB_TOKEN = "secret123"; 11 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 12 | 13 | // set other env variables so action-toolkit is happy 14 | process.env.GITHUB_REF = ""; 15 | process.env.GITHUB_WORKSPACE = ""; 16 | process.env.GITHUB_WORKFLOW = ""; 17 | process.env.GITHUB_ACTION = "twitter-together"; 18 | process.env.GITHUB_ACTOR = ""; 19 | process.env.GITHUB_REPOSITORY = ""; 20 | process.env.GITHUB_SHA = ""; 21 | 22 | // MOCK 23 | nock("https://api.github.com", { 24 | reqheaders: { 25 | authorization: "token secret123", 26 | }, 27 | }) 28 | // get changed files 29 | .get("/repos/twitter-together/action/pulls/123/files") 30 | .reply(200, [ 31 | { 32 | status: "updated", 33 | filename: "tweets/hello-world.tweet", 34 | }, 35 | ]); 36 | 37 | process.on("exit", (code) => { 38 | tap.equal(code, 0); 39 | tap.same(nock.pendingMocks(), []); 40 | }); 41 | 42 | require("../../lib"); 43 | -------------------------------------------------------------------------------- /test/pull-request-request-error/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "opened", 3 | "pull_request": { 4 | "number": 123, 5 | "base": { 6 | "ref": "main" 7 | }, 8 | "head": { 9 | "sha": "0000000000000000000000000000000000000002", 10 | "repo": { 11 | "fork": false 12 | } 13 | } 14 | }, 15 | "repository": { 16 | "default_branch": "main", 17 | "name": "action", 18 | "owner": { 19 | "login": "twitter-together" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/pull-request-request-error/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks a 500 server response when crying to retrieve pull request files 3 | */ 4 | 5 | const nock = require("nock"); 6 | const tap = require("tap"); 7 | 8 | // SETUP 9 | process.env.GITHUB_EVENT_NAME = "pull_request"; 10 | process.env.GITHUB_TOKEN = "secret123"; 11 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 12 | 13 | // set other env variables so action-toolkit is happy 14 | process.env.GITHUB_REF = ""; 15 | process.env.GITHUB_WORKSPACE = ""; 16 | process.env.GITHUB_WORKFLOW = ""; 17 | process.env.GITHUB_ACTION = "twitter-together"; 18 | process.env.GITHUB_ACTOR = ""; 19 | process.env.GITHUB_REPOSITORY = ""; 20 | process.env.GITHUB_SHA = ""; 21 | 22 | // MOCK 23 | nock("https://api.github.com", { 24 | reqheaders: { 25 | authorization: "token secret123", 26 | }, 27 | }) 28 | // get changed files 29 | .get("/repos/twitter-together/action/pulls/123/files") 30 | .reply(500); 31 | 32 | process.on("exit", (code) => { 33 | tap.equal(code, 1); 34 | tap.same(nock.pendingMocks(), []); 35 | 36 | // above code exits with 1 (error), but tap expects 0. 37 | // Tap adds the "process.exitCode" property for that purpose. 38 | process.exitCode = 0; 39 | }); 40 | 41 | require("../../lib"); 42 | -------------------------------------------------------------------------------- /test/pull-request-wrong-base/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "opened", 3 | "pull_request": { 4 | "number": 123, 5 | "base": { 6 | "ref": "patch" 7 | }, 8 | "head": { 9 | "sha": "0000000000000000000000000000000000000002", 10 | "repo": { 11 | "fork": false 12 | } 13 | } 14 | }, 15 | "repository": { 16 | "default_branch": "main", 17 | "name": "action", 18 | "owner": { 19 | "login": "twitter-together" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/pull-request-wrong-base/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of pull request adding a new *.tweet file 3 | */ 4 | 5 | const tap = require("tap"); 6 | const nock = require("nock"); 7 | 8 | // SETUP 9 | process.env.GITHUB_EVENT_NAME = "pull_request"; 10 | process.env.GITHUB_TOKEN = "secret123"; 11 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 12 | 13 | // set other env variables so action-toolkit is happy 14 | process.env.GITHUB_REF = ""; 15 | process.env.GITHUB_WORKSPACE = ""; 16 | process.env.GITHUB_WORKFLOW = ""; 17 | process.env.GITHUB_ACTION = "twitter-together"; 18 | process.env.GITHUB_ACTOR = ""; 19 | process.env.GITHUB_REPOSITORY = ""; 20 | process.env.GITHUB_SHA = ""; 21 | 22 | process.on("exit", (code) => { 23 | tap.equal(code, 0); 24 | tap.same(nock.pendingMocks(), []); 25 | 26 | // for some reason, tap fails with "Suites: 1 failed" if we don't exit explicitly 27 | process.exit(0); 28 | }); 29 | 30 | require("../../lib"); 31 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-front-matter-media/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/main", 3 | "before": "0000000000000000000000000000000000000001", 4 | "after": "0000000000000000000000000000000000000002", 5 | "head_commit": { 6 | "id": "0000000000000000000000000000000000000002" 7 | }, 8 | "repository": { 9 | "owner": { 10 | "login": "twitter-together" 11 | }, 12 | "name": "action", 13 | "default_branch": "main" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-front-matter-media/media/blahaj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twitter-together/action/08857009da2aacd9bd08204550ec96e15d76b4da/test/push-main-has-tweet-with-front-matter-media/media/blahaj.png -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-front-matter-media/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of a commit to the main branch 3 | * which includes a new *.tweet file that is making use of the front matter to quote retweet. 4 | */ 5 | 6 | const path = require("path"); 7 | 8 | const nock = require("nock"); 9 | const tap = require("tap"); 10 | 11 | // SETUP 12 | process.env.GITHUB_EVENT_NAME = "push"; 13 | process.env.GITHUB_TOKEN = "secret123"; 14 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 15 | process.env.GITHUB_REF = "refs/heads/main"; 16 | process.env.GITHUB_WORKSPACE = path.dirname(process.env.GITHUB_EVENT_PATH); 17 | process.env.TWITTER_API_KEY = "key123"; 18 | process.env.TWITTER_API_SECRET_KEY = "keysecret123"; 19 | process.env.TWITTER_ACCESS_TOKEN = "token123"; 20 | process.env.TWITTER_ACCESS_TOKEN_SECRET = "tokensecret123"; 21 | 22 | // set other env variables so action-toolkit is happy 23 | process.env.GITHUB_WORKFLOW = ""; 24 | process.env.GITHUB_ACTION = "twitter-together"; 25 | process.env.GITHUB_ACTOR = ""; 26 | process.env.GITHUB_REPOSITORY = ""; 27 | process.env.GITHUB_SHA = ""; 28 | 29 | // MOCK 30 | nock("https://api.github.com", { 31 | reqheaders: { 32 | authorization: "token secret123", 33 | }, 34 | }) 35 | // get changed files 36 | .get( 37 | "/repos/twitter-together/action/compare/0000000000000000000000000000000000000001...0000000000000000000000000000000000000002" 38 | ) 39 | .reply(200, { 40 | files: [ 41 | { 42 | status: "added", 43 | filename: "tweets/hello-world.tweet", 44 | }, 45 | { 46 | status: "added", 47 | filename: "media/blahaj.png", 48 | }, 49 | ], 50 | }) 51 | 52 | // post comment 53 | .post( 54 | "/repos/twitter-together/action/commits/0000000000000000000000000000000000000002/comments", 55 | (body) => { 56 | tap.equal( 57 | body.body, 58 | "Tweeted:\n\n- https://twitter.com/gr2m/status/0000000000000000001" 59 | ); 60 | return true; 61 | } 62 | ) 63 | .reply(201); 64 | 65 | nock("https://api.twitter.com") 66 | .get("/2/users/me") 67 | .reply(200, { 68 | data: { 69 | id: "123", 70 | name: "gr2m", 71 | username: "gr2m", 72 | }, 73 | }) 74 | 75 | .post("/2/tweets", (body) => { 76 | tap.equal(body.text, "Cuddly :)"); 77 | tap.type(body.media, "object"); 78 | tap.hasProp(body.media, "media_ids"); 79 | tap.same(body.media.media_ids, ["0000000000000000002"]); 80 | return true; 81 | }) 82 | .reply(201, { 83 | data: { 84 | id: "0000000000000000001", 85 | text: "Cuddly :) https://t.co/abcdeFGHIJ", 86 | }, 87 | }); 88 | 89 | nock("https://upload.twitter.com") 90 | .post("/1.1/media/upload.json", (body) => { 91 | tap.match( 92 | body, 93 | 'Content-Disposition: form-data; name="command"\r\n\r\nINIT' 94 | ); 95 | tap.match( 96 | body, 97 | 'Content-Disposition: form-data; name="total_bytes"\r\n\r\n107352' 98 | ); 99 | tap.match( 100 | body, 101 | 'Content-Disposition: form-data; name="media_type"\r\n\r\nimage/png' 102 | ); 103 | return true; 104 | }) 105 | .reply(202, { 106 | media_id_string: "0000000000000000002", 107 | expires_after_secs: 86400, 108 | media_key: "3_0000000000000000002", 109 | }) 110 | 111 | .post("/1.1/media/upload.json") 112 | .reply(204) 113 | 114 | .post("/1.1/media/upload.json", (body) => { 115 | tap.match( 116 | body, 117 | 'Content-Disposition: form-data; name="command"\r\n\r\nFINALIZE' 118 | ); 119 | tap.match( 120 | body, 121 | 'Content-Disposition: form-data; name="media_id"\r\n\r\n0000000000000000002' 122 | ); 123 | return true; 124 | }) 125 | .reply(201, { 126 | media_id_string: "0000000000000000002", 127 | media_key: "3_0000000000000000002", 128 | size: 107352, 129 | expires_after_secs: 86400, 130 | image: { 131 | image_type: "image/png", 132 | w: 640, 133 | h: 360, 134 | }, 135 | }) 136 | 137 | .post("/1.1/media/metadata/create.json", (body) => { 138 | tap.equal(body.media_id, "0000000000000000002"); 139 | tap.type(body.alt_text, "object"); 140 | tap.hasProp(body.alt_text, "text"); 141 | tap.equal(body.alt_text.text, "Blahaj!"); 142 | return true; 143 | }) 144 | .reply(200); 145 | 146 | process.on("exit", (code) => { 147 | tap.equal(code, 0); 148 | tap.same(nock.pendingMocks(), []); 149 | }); 150 | 151 | require("../../lib"); 152 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-front-matter-media/tweets/hello-world.tweet: -------------------------------------------------------------------------------- 1 | --- 2 | media: 3 | - file: blahaj.png 4 | alt: Blahaj! 5 | --- 6 | 7 | Cuddly :) 8 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-front-matter-poll/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/main", 3 | "before": "0000000000000000000000000000000000000001", 4 | "after": "0000000000000000000000000000000000000002", 5 | "head_commit": { 6 | "id": "0000000000000000000000000000000000000002" 7 | }, 8 | "repository": { 9 | "owner": { 10 | "login": "twitter-together" 11 | }, 12 | "name": "action", 13 | "default_branch": "main" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-front-matter-poll/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of a commit to the main branch 3 | * which includes a new *.tweet file that is making use of the front matter for a poll. 4 | */ 5 | 6 | const path = require("path"); 7 | 8 | const nock = require("nock"); 9 | const tap = require("tap"); 10 | 11 | // SETUP 12 | process.env.GITHUB_EVENT_NAME = "push"; 13 | process.env.GITHUB_TOKEN = "secret123"; 14 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 15 | process.env.GITHUB_REF = "refs/heads/main"; 16 | process.env.GITHUB_WORKSPACE = path.dirname(process.env.GITHUB_EVENT_PATH); 17 | process.env.TWITTER_API_KEY = "key123"; 18 | process.env.TWITTER_API_SECRET_KEY = "keysecret123"; 19 | process.env.TWITTER_ACCESS_TOKEN = "token123"; 20 | process.env.TWITTER_ACCESS_TOKEN_SECRET = "tokensecret123"; 21 | 22 | // set other env variables so action-toolkit is happy 23 | process.env.GITHUB_WORKFLOW = ""; 24 | process.env.GITHUB_ACTION = "twitter-together"; 25 | process.env.GITHUB_ACTOR = ""; 26 | process.env.GITHUB_REPOSITORY = ""; 27 | process.env.GITHUB_SHA = ""; 28 | 29 | // MOCK 30 | nock("https://api.github.com", { 31 | reqheaders: { 32 | authorization: "token secret123", 33 | }, 34 | }) 35 | // get changed files 36 | .get( 37 | "/repos/twitter-together/action/compare/0000000000000000000000000000000000000001...0000000000000000000000000000000000000002" 38 | ) 39 | .reply(200, { 40 | files: [ 41 | { 42 | status: "added", 43 | filename: "tweets/hello-world.tweet", 44 | }, 45 | ], 46 | }) 47 | 48 | // post comment 49 | .post( 50 | "/repos/twitter-together/action/commits/0000000000000000000000000000000000000002/comments", 51 | (body) => { 52 | tap.equal( 53 | body.body, 54 | "Tweeted:\n\n- https://twitter.com/gr2m/status/0000000000000000001" 55 | ); 56 | return true; 57 | } 58 | ) 59 | .reply(201); 60 | 61 | nock("https://api.twitter.com") 62 | .get("/2/users/me") 63 | .reply(200, { 64 | data: { 65 | id: "123", 66 | name: "gr2m", 67 | username: "gr2m", 68 | }, 69 | }) 70 | 71 | .post("/2/tweets", (body) => { 72 | tap.equal(body.text, "Hello, world!"); 73 | tap.type(body.poll, "object"); 74 | tap.hasProp(body.poll, "options"); 75 | tap.same(body.poll.options, ["a", "b"]); 76 | tap.hasProp(body.poll, "duration_minutes"); 77 | tap.equal(body.poll.duration_minutes, 1440); 78 | return true; 79 | }) 80 | .reply(201, { 81 | data: { 82 | id: "0000000000000000001", 83 | text: "Hello, world!", 84 | }, 85 | }); 86 | 87 | process.on("exit", (code) => { 88 | tap.equal(code, 0); 89 | tap.same(nock.pendingMocks(), []); 90 | }); 91 | 92 | require("../../lib"); 93 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-front-matter-poll/tweets/hello-world.tweet: -------------------------------------------------------------------------------- 1 | --- 2 | poll: 3 | - a 4 | - b 5 | --- 6 | 7 | Hello, world! 8 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-front-matter-quote/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/main", 3 | "before": "0000000000000000000000000000000000000001", 4 | "after": "0000000000000000000000000000000000000002", 5 | "head_commit": { 6 | "id": "0000000000000000000000000000000000000002" 7 | }, 8 | "repository": { 9 | "owner": { 10 | "login": "twitter-together" 11 | }, 12 | "name": "action", 13 | "default_branch": "main" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-front-matter-quote/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of a commit to the main branch 3 | * which includes a new *.tweet file that is making use of the front matter to quote retweet. 4 | */ 5 | 6 | const path = require("path"); 7 | 8 | const nock = require("nock"); 9 | const tap = require("tap"); 10 | 11 | // SETUP 12 | process.env.GITHUB_EVENT_NAME = "push"; 13 | process.env.GITHUB_TOKEN = "secret123"; 14 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 15 | process.env.GITHUB_REF = "refs/heads/main"; 16 | process.env.GITHUB_WORKSPACE = path.dirname(process.env.GITHUB_EVENT_PATH); 17 | process.env.TWITTER_API_KEY = "key123"; 18 | process.env.TWITTER_API_SECRET_KEY = "keysecret123"; 19 | process.env.TWITTER_ACCESS_TOKEN = "token123"; 20 | process.env.TWITTER_ACCESS_TOKEN_SECRET = "tokensecret123"; 21 | 22 | // set other env variables so action-toolkit is happy 23 | process.env.GITHUB_WORKFLOW = ""; 24 | process.env.GITHUB_ACTION = "twitter-together"; 25 | process.env.GITHUB_ACTOR = ""; 26 | process.env.GITHUB_REPOSITORY = ""; 27 | process.env.GITHUB_SHA = ""; 28 | 29 | // MOCK 30 | nock("https://api.github.com", { 31 | reqheaders: { 32 | authorization: "token secret123", 33 | }, 34 | }) 35 | // get changed files 36 | .get( 37 | "/repos/twitter-together/action/compare/0000000000000000000000000000000000000001...0000000000000000000000000000000000000002" 38 | ) 39 | .reply(200, { 40 | files: [ 41 | { 42 | status: "added", 43 | filename: "tweets/hello-world.tweet", 44 | }, 45 | ], 46 | }) 47 | 48 | // post comment 49 | .post( 50 | "/repos/twitter-together/action/commits/0000000000000000000000000000000000000002/comments", 51 | (body) => { 52 | tap.equal( 53 | body.body, 54 | "Tweeted:\n\n- https://twitter.com/gr2m/status/0000000000000000002" 55 | ); 56 | return true; 57 | } 58 | ) 59 | .reply(201); 60 | 61 | nock("https://api.twitter.com") 62 | .get("/2/users/me") 63 | .reply(200, { 64 | data: { 65 | id: "123", 66 | name: "gr2m", 67 | username: "gr2m", 68 | }, 69 | }) 70 | 71 | .post("/2/tweets", (body) => { 72 | tap.equal(body.text, "Smart thinking!"); 73 | tap.equal(body.quote_tweet_id, "0000000000000000001"); 74 | return true; 75 | }) 76 | .reply(201, { 77 | data: { 78 | id: "0000000000000000002", 79 | text: "Smart thinking! https://t.co/abcdeFGHIJ", 80 | }, 81 | }); 82 | 83 | process.on("exit", (code) => { 84 | tap.equal(code, 0); 85 | tap.same(nock.pendingMocks(), []); 86 | }); 87 | 88 | require("../../lib"); 89 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-front-matter-quote/tweets/hello-world.tweet: -------------------------------------------------------------------------------- 1 | --- 2 | retweet: https://twitter.com/m2rg/status/0000000000000000001 3 | --- 4 | 5 | Smart thinking! 6 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-front-matter-reply/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/main", 3 | "before": "0000000000000000000000000000000000000001", 4 | "after": "0000000000000000000000000000000000000002", 5 | "head_commit": { 6 | "id": "0000000000000000000000000000000000000002" 7 | }, 8 | "repository": { 9 | "owner": { 10 | "login": "twitter-together" 11 | }, 12 | "name": "action", 13 | "default_branch": "main" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-front-matter-reply/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of a commit to the main branch 3 | * which includes a new *.tweet file that is making use of the front matter to reply. 4 | */ 5 | 6 | const path = require("path"); 7 | 8 | const nock = require("nock"); 9 | const tap = require("tap"); 10 | 11 | // SETUP 12 | process.env.GITHUB_EVENT_NAME = "push"; 13 | process.env.GITHUB_TOKEN = "secret123"; 14 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 15 | process.env.GITHUB_REF = "refs/heads/main"; 16 | process.env.GITHUB_WORKSPACE = path.dirname(process.env.GITHUB_EVENT_PATH); 17 | process.env.TWITTER_API_KEY = "key123"; 18 | process.env.TWITTER_API_SECRET_KEY = "keysecret123"; 19 | process.env.TWITTER_ACCESS_TOKEN = "token123"; 20 | process.env.TWITTER_ACCESS_TOKEN_SECRET = "tokensecret123"; 21 | 22 | // set other env variables so action-toolkit is happy 23 | process.env.GITHUB_WORKFLOW = ""; 24 | process.env.GITHUB_ACTION = "twitter-together"; 25 | process.env.GITHUB_ACTOR = ""; 26 | process.env.GITHUB_REPOSITORY = ""; 27 | process.env.GITHUB_SHA = ""; 28 | 29 | // MOCK 30 | nock("https://api.github.com", { 31 | reqheaders: { 32 | authorization: "token secret123", 33 | }, 34 | }) 35 | // get changed files 36 | .get( 37 | "/repos/twitter-together/action/compare/0000000000000000000000000000000000000001...0000000000000000000000000000000000000002" 38 | ) 39 | .reply(200, { 40 | files: [ 41 | { 42 | status: "added", 43 | filename: "tweets/hello-world.tweet", 44 | }, 45 | ], 46 | }) 47 | 48 | // post comment 49 | .post( 50 | "/repos/twitter-together/action/commits/0000000000000000000000000000000000000002/comments", 51 | (body) => { 52 | tap.equal( 53 | body.body, 54 | "Tweeted:\n\n- https://twitter.com/gr2m/status/0000000000000000002" 55 | ); 56 | return true; 57 | } 58 | ) 59 | .reply(201); 60 | 61 | nock("https://api.twitter.com") 62 | .get("/2/users/me") 63 | .reply(200, { 64 | data: { 65 | id: "123", 66 | name: "gr2m", 67 | username: "gr2m", 68 | }, 69 | }) 70 | 71 | .post("/2/tweets", (body) => { 72 | tap.equal(body.text, "Good idea :)"); 73 | tap.type(body.reply, "object"); 74 | tap.hasProp(body.reply, "in_reply_to_tweet_id"); 75 | tap.equal(body.reply.in_reply_to_tweet_id, "0000000000000000001"); 76 | return true; 77 | }) 78 | .reply(201, { 79 | data: { 80 | id: "0000000000000000002", 81 | text: "Good idea :)", 82 | }, 83 | }); 84 | 85 | process.on("exit", (code) => { 86 | tap.equal(code, 0); 87 | tap.same(nock.pendingMocks(), []); 88 | }); 89 | 90 | require("../../lib"); 91 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-front-matter-reply/tweets/hello-world.tweet: -------------------------------------------------------------------------------- 1 | --- 2 | reply: https://twitter.com/gr2m/status/0000000000000000001 3 | --- 4 | 5 | Good idea :) 6 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-front-matter-retweet/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/main", 3 | "before": "0000000000000000000000000000000000000001", 4 | "after": "0000000000000000000000000000000000000002", 5 | "head_commit": { 6 | "id": "0000000000000000000000000000000000000002" 7 | }, 8 | "repository": { 9 | "owner": { 10 | "login": "twitter-together" 11 | }, 12 | "name": "action", 13 | "default_branch": "main" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-front-matter-retweet/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of a commit to the main branch 3 | * which includes a new *.tweet file that is making use of the front matter to retweet. 4 | */ 5 | 6 | const path = require("path"); 7 | 8 | const nock = require("nock"); 9 | const tap = require("tap"); 10 | 11 | // SETUP 12 | process.env.GITHUB_EVENT_NAME = "push"; 13 | process.env.GITHUB_TOKEN = "secret123"; 14 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 15 | process.env.GITHUB_REF = "refs/heads/main"; 16 | process.env.GITHUB_WORKSPACE = path.dirname(process.env.GITHUB_EVENT_PATH); 17 | process.env.TWITTER_API_KEY = "key123"; 18 | process.env.TWITTER_API_SECRET_KEY = "keysecret123"; 19 | process.env.TWITTER_ACCESS_TOKEN = "token123"; 20 | process.env.TWITTER_ACCESS_TOKEN_SECRET = "tokensecret123"; 21 | 22 | // set other env variables so action-toolkit is happy 23 | process.env.GITHUB_WORKFLOW = ""; 24 | process.env.GITHUB_ACTION = "twitter-together"; 25 | process.env.GITHUB_ACTOR = ""; 26 | process.env.GITHUB_REPOSITORY = ""; 27 | process.env.GITHUB_SHA = ""; 28 | 29 | // MOCK 30 | nock("https://api.github.com", { 31 | reqheaders: { 32 | authorization: "token secret123", 33 | }, 34 | }) 35 | // get changed files 36 | .get( 37 | "/repos/twitter-together/action/compare/0000000000000000000000000000000000000001...0000000000000000000000000000000000000002" 38 | ) 39 | .reply(200, { 40 | files: [ 41 | { 42 | status: "added", 43 | filename: "tweets/hello-world.tweet", 44 | }, 45 | ], 46 | }) 47 | 48 | // post comment 49 | .post( 50 | "/repos/twitter-together/action/commits/0000000000000000000000000000000000000002/comments", 51 | (body) => { 52 | tap.equal( 53 | body.body, 54 | "Tweeted:\n\n- https://twitter.com/m2rg/status/0000000000000000001" 55 | ); 56 | return true; 57 | } 58 | ) 59 | .reply(201); 60 | 61 | nock("https://api.twitter.com") 62 | .get("/2/users/me") 63 | .reply(200, { 64 | data: { 65 | id: "123", 66 | name: "gr2m", 67 | username: "gr2m", 68 | }, 69 | }) 70 | 71 | .post("/2/users/123/retweets", (body) => { 72 | tap.equal(body.tweet_id, "0000000000000000001"); 73 | return true; 74 | }) 75 | .reply(201, { 76 | data: { 77 | retweeted: true, 78 | }, 79 | }) 80 | 81 | .get("/2/tweets/0000000000000000001?expansions=author_id") 82 | .reply(200, { 83 | data: { 84 | id: "0000000000000000001", 85 | text: "", 86 | author_id: "456", 87 | }, 88 | includes: { 89 | users: [ 90 | { 91 | id: "456", 92 | name: "m2rg", 93 | username: "m2rg", 94 | }, 95 | ], 96 | }, 97 | }); 98 | 99 | process.on("exit", (code) => { 100 | tap.equal(code, 0); 101 | tap.same(nock.pendingMocks(), []); 102 | }); 103 | 104 | require("../../lib"); 105 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-front-matter-retweet/tweets/hello-world.tweet: -------------------------------------------------------------------------------- 1 | --- 2 | retweet: https://twitter.com/m2rg/status/0000000000000000001 3 | --- 4 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-poll/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/main", 3 | "before": "0000000000000000000000000000000000000001", 4 | "after": "0000000000000000000000000000000000000002", 5 | "head_commit": { 6 | "id": "0000000000000000000000000000000000000002" 7 | }, 8 | "repository": { 9 | "owner": { 10 | "login": "twitter-together" 11 | }, 12 | "name": "action", 13 | "default_branch": "main" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-poll/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of a commit to the main branch (main) 3 | * which includes a new *.tweet file. 4 | */ 5 | 6 | const path = require("path"); 7 | 8 | const nock = require("nock"); 9 | const tap = require("tap"); 10 | 11 | // SETUP 12 | process.env.GITHUB_EVENT_NAME = "push"; 13 | process.env.GITHUB_TOKEN = "secret123"; 14 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 15 | process.env.GITHUB_REF = "refs/heads/main"; 16 | process.env.GITHUB_WORKSPACE = path.dirname(process.env.GITHUB_EVENT_PATH); 17 | process.env.TWITTER_API_KEY = "key123"; 18 | process.env.TWITTER_API_SECRET_KEY = "keysecret123"; 19 | process.env.TWITTER_ACCESS_TOKEN = "token123"; 20 | process.env.TWITTER_ACCESS_TOKEN_SECRET = "tokensecret123"; 21 | 22 | // set other env variables so action-toolkit is happy 23 | process.env.GITHUB_WORKFLOW = ""; 24 | process.env.GITHUB_ACTION = "twitter-together"; 25 | process.env.GITHUB_ACTOR = ""; 26 | process.env.GITHUB_REPOSITORY = ""; 27 | process.env.GITHUB_SHA = ""; 28 | 29 | // MOCK 30 | nock("https://api.github.com", { 31 | reqheaders: { 32 | authorization: "token secret123", 33 | }, 34 | }) 35 | // get changed files 36 | .get( 37 | "/repos/twitter-together/action/compare/0000000000000000000000000000000000000001...0000000000000000000000000000000000000002" 38 | ) 39 | .reply(200, { 40 | files: [ 41 | { 42 | status: "added", 43 | filename: "tweets/my-poll.tweet", 44 | }, 45 | ], 46 | }) 47 | 48 | // post comment 49 | .post( 50 | "/repos/twitter-together/action/commits/0000000000000000000000000000000000000002/comments", 51 | (body) => { 52 | tap.equal( 53 | body.body, 54 | "Tweeted:\n\n- https://twitter.com/gr2m/status/0000000000000000001" 55 | ); 56 | return true; 57 | } 58 | ) 59 | .reply(201); 60 | 61 | nock("https://api.twitter.com") 62 | .get("/2/users/me") 63 | .reply(200, { 64 | data: { 65 | id: "123", 66 | name: "gr2m", 67 | username: "gr2m", 68 | }, 69 | }) 70 | 71 | .post("/2/tweets", (body) => { 72 | tap.equal(body.text, "Here is my poll"); 73 | tap.type(body.poll, "object"); 74 | tap.hasProp(body.poll, "options"); 75 | tap.same(body.poll.options, [ 76 | "option 1", 77 | "option 2", 78 | "option 3", 79 | "option 4", 80 | ]); 81 | tap.hasProp(body.poll, "duration_minutes"); 82 | tap.equal(body.poll.duration_minutes, 1440); 83 | return true; 84 | }) 85 | .reply(201, { 86 | data: { 87 | id: "0000000000000000001", 88 | text: "Here is my poll", 89 | }, 90 | }); 91 | 92 | process.on("exit", (code) => { 93 | tap.equal(code, 0); 94 | tap.same(nock.pendingMocks(), []); 95 | }); 96 | 97 | require("../../lib"); 98 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-poll/tweets/my-poll.tweet: -------------------------------------------------------------------------------- 1 | Here is my poll 2 | 3 | ( ) option 1 4 | () option 2 5 | ( ) option 3 6 | () option 4 -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-thread/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/main", 3 | "before": "0000000000000000000000000000000000000001", 4 | "after": "0000000000000000000000000000000000000002", 5 | "head_commit": { 6 | "id": "0000000000000000000000000000000000000002" 7 | }, 8 | "repository": { 9 | "owner": { 10 | "login": "twitter-together" 11 | }, 12 | "name": "action", 13 | "default_branch": "main" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-thread/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of a commit to the main branch 3 | * which includes a new *.tweet file that is making use of the front matter. 4 | * 5 | * Only threads and polls are supported as part of . 6 | */ 7 | 8 | const path = require("path"); 9 | 10 | const nock = require("nock"); 11 | const tap = require("tap"); 12 | 13 | // SETUP 14 | process.env.GITHUB_EVENT_NAME = "push"; 15 | process.env.GITHUB_TOKEN = "secret123"; 16 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 17 | process.env.GITHUB_REF = "refs/heads/main"; 18 | process.env.GITHUB_WORKSPACE = path.dirname(process.env.GITHUB_EVENT_PATH); 19 | process.env.TWITTER_API_KEY = "key123"; 20 | process.env.TWITTER_API_SECRET_KEY = "keysecret123"; 21 | process.env.TWITTER_ACCESS_TOKEN = "token123"; 22 | process.env.TWITTER_ACCESS_TOKEN_SECRET = "tokensecret123"; 23 | 24 | // set other env variables so action-toolkit is happy 25 | process.env.GITHUB_WORKFLOW = ""; 26 | process.env.GITHUB_ACTION = "twitter-together"; 27 | process.env.GITHUB_ACTOR = ""; 28 | process.env.GITHUB_REPOSITORY = ""; 29 | process.env.GITHUB_SHA = ""; 30 | 31 | // MOCK 32 | nock("https://api.github.com", { 33 | reqheaders: { 34 | authorization: "token secret123", 35 | }, 36 | }) 37 | // get changed files 38 | .get( 39 | "/repos/twitter-together/action/compare/0000000000000000000000000000000000000001...0000000000000000000000000000000000000002" 40 | ) 41 | .reply(200, { 42 | files: [ 43 | { 44 | status: "added", 45 | filename: "tweets/hello-world.tweet", 46 | }, 47 | ], 48 | }) 49 | 50 | // post comment 51 | .post( 52 | "/repos/twitter-together/action/commits/0000000000000000000000000000000000000002/comments", 53 | (body) => { 54 | tap.equal( 55 | body.body, 56 | "Tweeted:\n\n- https://twitter.com/gr2m/status/0000000000000000001\n- https://twitter.com/gr2m/status/0000000000000000002" 57 | ); 58 | return true; 59 | } 60 | ) 61 | .reply(201); 62 | 63 | nock("https://api.twitter.com") 64 | .get("/2/users/me") 65 | .reply(200, { 66 | data: { 67 | id: "123", 68 | name: "gr2m", 69 | username: "gr2m", 70 | }, 71 | }) 72 | 73 | .post("/2/tweets", (body) => { 74 | tap.equal(body.text, "Hello, world!"); 75 | return true; 76 | }) 77 | .reply(201, { 78 | data: { 79 | id: "0000000000000000001", 80 | text: "Hello, world!", 81 | }, 82 | }) 83 | 84 | .post("/2/tweets", (body) => { 85 | tap.equal(body.text, "Second Tweet!"); 86 | tap.type(body.reply, "object"); 87 | tap.hasProp(body.reply, "in_reply_to_tweet_id"); 88 | tap.equal(body.reply.in_reply_to_tweet_id, "0000000000000000001"); 89 | return true; 90 | }) 91 | .reply(201, { 92 | data: { 93 | id: "0000000000000000002", 94 | text: "Second Tweet!", 95 | }, 96 | }); 97 | 98 | process.on("exit", (code) => { 99 | tap.equal(code, 0); 100 | tap.same(nock.pendingMocks(), []); 101 | }); 102 | 103 | require("../../lib"); 104 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-thread/tweets/hello-world.tweet: -------------------------------------------------------------------------------- 1 | Hello, world! 2 | 3 | --- 4 | 5 | Second Tweet! 6 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-trailing-whitespace/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/main", 3 | "before": "0000000000000000000000000000000000000001", 4 | "after": "0000000000000000000000000000000000000002", 5 | "head_commit": { 6 | "id": "0000000000000000000000000000000000000002" 7 | }, 8 | "repository": { 9 | "owner": { 10 | "login": "twitter-together" 11 | }, 12 | "name": "action", 13 | "default_branch": "main" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-trailing-whitespace/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of a commit to the main branch 3 | * which includes a new *.tweet file that has trailing whitespace on front matter and thread delimiters 4 | */ 5 | 6 | const path = require("path"); 7 | const fs = require("fs"); 8 | 9 | const nock = require("nock"); 10 | const tap = require("tap"); 11 | 12 | // SETUP 13 | process.env.GITHUB_EVENT_NAME = "push"; 14 | process.env.GITHUB_TOKEN = "secret123"; 15 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 16 | process.env.GITHUB_REF = "refs/heads/main"; 17 | process.env.GITHUB_WORKSPACE = path.dirname(process.env.GITHUB_EVENT_PATH); 18 | process.env.TWITTER_API_KEY = "key123"; 19 | process.env.TWITTER_API_SECRET_KEY = "keysecret123"; 20 | process.env.TWITTER_ACCESS_TOKEN = "token123"; 21 | process.env.TWITTER_ACCESS_TOKEN_SECRET = "tokensecret123"; 22 | 23 | // set other env variables so action-toolkit is happy 24 | process.env.GITHUB_WORKFLOW = ""; 25 | process.env.GITHUB_ACTION = "twitter-together"; 26 | process.env.GITHUB_ACTOR = ""; 27 | process.env.GITHUB_REPOSITORY = ""; 28 | process.env.GITHUB_SHA = ""; 29 | 30 | // MOCK 31 | nock("https://api.github.com", { 32 | reqheaders: { 33 | authorization: "token secret123", 34 | }, 35 | }) 36 | // get changed files 37 | .get( 38 | "/repos/twitter-together/action/compare/0000000000000000000000000000000000000001...0000000000000000000000000000000000000002" 39 | ) 40 | .reply(200, { 41 | files: [ 42 | { 43 | status: "added", 44 | filename: "tweets/hello-world.tweet", 45 | }, 46 | ], 47 | }) 48 | 49 | // post comment 50 | .post( 51 | "/repos/twitter-together/action/commits/0000000000000000000000000000000000000002/comments", 52 | (body) => { 53 | tap.equal( 54 | body.body, 55 | "Tweeted:\n\n- https://twitter.com/gr2m/status/0000000000000000002\n- https://twitter.com/gr2m/status/0000000000000000003" 56 | ); 57 | return true; 58 | } 59 | ) 60 | .reply(201); 61 | 62 | nock("https://api.twitter.com") 63 | .get("/2/users/me") 64 | .reply(200, { 65 | data: { 66 | id: "123", 67 | name: "gr2m", 68 | username: "gr2m", 69 | }, 70 | }) 71 | 72 | .post("/2/tweets", (body) => { 73 | tap.equal(body.text, "Hello, world!"); 74 | tap.type(body.reply, "object"); 75 | tap.hasProp(body.reply, "in_reply_to_tweet_id"); 76 | tap.equal(body.reply.in_reply_to_tweet_id, "0000000000000000001"); 77 | return true; 78 | }) 79 | .reply(201, { 80 | data: { 81 | id: "0000000000000000002", 82 | text: "Hello, world!", 83 | }, 84 | }) 85 | 86 | .post("/2/tweets", (body) => { 87 | tap.equal(body.text, "Second Tweet!"); 88 | tap.type(body.reply, "object"); 89 | tap.hasProp(body.reply, "in_reply_to_tweet_id"); 90 | tap.equal(body.reply.in_reply_to_tweet_id, "0000000000000000002"); 91 | return true; 92 | }) 93 | .reply(201, { 94 | data: { 95 | id: "0000000000000000003", 96 | text: "Second Tweet!", 97 | }, 98 | }); 99 | 100 | // Confirm there is whitespace 101 | const contents = fs.readFileSync( 102 | path.join(__dirname, "tweets/hello-world.tweet"), 103 | "utf-8" 104 | ); 105 | tap.match(contents, /^--- \n[\s\S]+\n--- \n[\s\S]+\n--- \n[\s\S]+$/); 106 | 107 | process.on("exit", (code) => { 108 | tap.equal(code, 0); 109 | tap.same(nock.pendingMocks(), []); 110 | }); 111 | 112 | require("../../lib"); 113 | -------------------------------------------------------------------------------- /test/push-main-has-tweet-with-trailing-whitespace/tweets/hello-world.tweet: -------------------------------------------------------------------------------- 1 | --- 2 | reply: https://twitter.com/gr2m/status/0000000000000000001 3 | --- 4 | 5 | Hello, world! 6 | 7 | --- 8 | 9 | Second Tweet! 10 | -------------------------------------------------------------------------------- /test/push-main-has-tweet/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/main", 3 | "before": "0000000000000000000000000000000000000001", 4 | "after": "0000000000000000000000000000000000000002", 5 | "head_commit": { 6 | "id": "0000000000000000000000000000000000000002" 7 | }, 8 | "repository": { 9 | "owner": { 10 | "login": "twitter-together" 11 | }, 12 | "name": "action", 13 | "default_branch": "main" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/push-main-has-tweet/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of a commit to the main branch (main) 3 | * which includes a new *.tweet file. 4 | */ 5 | 6 | const path = require("path"); 7 | 8 | const nock = require("nock"); 9 | const tap = require("tap"); 10 | 11 | // SETUP 12 | process.env.GITHUB_EVENT_NAME = "push"; 13 | process.env.GITHUB_TOKEN = "secret123"; 14 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 15 | process.env.GITHUB_REF = "refs/heads/main"; 16 | process.env.GITHUB_WORKSPACE = path.dirname(process.env.GITHUB_EVENT_PATH); 17 | process.env.TWITTER_API_KEY = "key123"; 18 | process.env.TWITTER_API_SECRET_KEY = "keysecret123"; 19 | process.env.TWITTER_ACCESS_TOKEN = "token123"; 20 | process.env.TWITTER_ACCESS_TOKEN_SECRET = "tokensecret123"; 21 | 22 | // set other env variables so action-toolkit is happy 23 | process.env.GITHUB_WORKFLOW = ""; 24 | process.env.GITHUB_ACTION = "twitter-together"; 25 | process.env.GITHUB_ACTOR = ""; 26 | process.env.GITHUB_REPOSITORY = ""; 27 | process.env.GITHUB_SHA = ""; 28 | 29 | // MOCK 30 | nock("https://api.github.com", { 31 | reqheaders: { 32 | authorization: "token secret123", 33 | }, 34 | }) 35 | // get changed files 36 | .get( 37 | "/repos/twitter-together/action/compare/0000000000000000000000000000000000000001...0000000000000000000000000000000000000002" 38 | ) 39 | .reply(200, { 40 | files: [ 41 | { 42 | status: "added", 43 | filename: "tweets/hello-world.tweet", 44 | }, 45 | ], 46 | }) 47 | 48 | // post comment 49 | .post( 50 | "/repos/twitter-together/action/commits/0000000000000000000000000000000000000002/comments", 51 | (body) => { 52 | tap.equal( 53 | body.body, 54 | "Tweeted:\n\n- https://twitter.com/gr2m/status/0000000000000000001" 55 | ); 56 | return true; 57 | } 58 | ) 59 | .reply(201); 60 | 61 | nock("https://api.twitter.com") 62 | .get("/2/users/me") 63 | .reply(200, { 64 | data: { 65 | id: "123", 66 | name: "gr2m", 67 | username: "gr2m", 68 | }, 69 | }) 70 | 71 | .post("/2/tweets", (body) => { 72 | tap.equal(body.text, "Hello, world!"); 73 | return true; 74 | }) 75 | .reply(201, { 76 | data: { 77 | id: "0000000000000000001", 78 | text: "Hello, world!", 79 | }, 80 | }); 81 | 82 | process.on("exit", (code) => { 83 | tap.equal(code, 0); 84 | tap.same(nock.pendingMocks(), []); 85 | }); 86 | 87 | require("../../lib"); 88 | -------------------------------------------------------------------------------- /test/push-main-has-tweet/tweets/hello-world.tweet: -------------------------------------------------------------------------------- 1 | Hello, world! 2 | -------------------------------------------------------------------------------- /test/push-main-no-tweets/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/main", 3 | "before": "0000000000000000000000000000000000000001", 4 | "after": "0000000000000000000000000000000000000002", 5 | "head_commit": { 6 | "id": "0000000000000000000000000000000000000002" 7 | }, 8 | "repository": { 9 | "owner": { 10 | "login": "twitter-together" 11 | }, 12 | "name": "action", 13 | "default_branch": "main" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/push-main-no-tweets/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of a commit to the main branch (main) 3 | * which includes a new *.tweet file. 4 | */ 5 | 6 | const path = require("path"); 7 | 8 | const nock = require("nock"); 9 | const tap = require("tap"); 10 | 11 | // SETUP 12 | process.env.GITHUB_EVENT_NAME = "push"; 13 | process.env.GITHUB_TOKEN = "secret123"; 14 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 15 | process.env.GITHUB_REF = "refs/heads/main"; 16 | process.env.GITHUB_WORKSPACE = path.dirname(process.env.GITHUB_EVENT_PATH); 17 | 18 | // set other env variables so action-toolkit is happy 19 | process.env.GITHUB_WORKFLOW = ""; 20 | process.env.GITHUB_ACTION = "twitter-together"; 21 | process.env.GITHUB_ACTOR = ""; 22 | process.env.GITHUB_REPOSITORY = ""; 23 | process.env.GITHUB_SHA = ""; 24 | 25 | // MOCK 26 | nock("https://api.github.com") 27 | // get changed files 28 | .get( 29 | "/repos/twitter-together/action/compare/0000000000000000000000000000000000000001...0000000000000000000000000000000000000002" 30 | ) 31 | .reply(200, { 32 | files: [ 33 | { 34 | status: "updated", 35 | filename: "tweets/hello-world.tweet", 36 | }, 37 | ], 38 | }); 39 | 40 | process.on("exit", (code) => { 41 | tap.equal(code, 0); 42 | tap.same(nock.pendingMocks(), []); 43 | 44 | // for some reason, tap fails with "Suites: 1 failed" if we don't exit explicitly 45 | process.exit(0); 46 | }); 47 | 48 | require("../../lib"); 49 | -------------------------------------------------------------------------------- /test/push-main-no-tweets/tweets/hello-world.tweet: -------------------------------------------------------------------------------- 1 | Hello, world! 2 | -------------------------------------------------------------------------------- /test/push-main-request-error/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/main", 3 | "before": "0000000000000000000000000000000000000001", 4 | "after": "0000000000000000000000000000000000000002", 5 | "head_commit": { 6 | "id": "0000000000000000000000000000000000000002" 7 | }, 8 | "repository": { 9 | "owner": { 10 | "login": "twitter-together" 11 | }, 12 | "name": "action", 13 | "default_branch": "main" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/push-main-request-error/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of a commit to the main branch (main) 3 | * which includes a new *.tweet file. 4 | */ 5 | 6 | const path = require("path"); 7 | 8 | const nock = require("nock"); 9 | const tap = require("tap"); 10 | 11 | // SETUP 12 | process.env.GITHUB_EVENT_NAME = "push"; 13 | process.env.GITHUB_TOKEN = "secret123"; 14 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 15 | process.env.GITHUB_REF = "refs/heads/main"; 16 | process.env.GITHUB_WORKSPACE = path.dirname(process.env.GITHUB_EVENT_PATH); 17 | 18 | // set other env variables so action-toolkit is happy 19 | process.env.GITHUB_WORKFLOW = ""; 20 | process.env.GITHUB_ACTION = "twitter-together"; 21 | process.env.GITHUB_ACTOR = ""; 22 | process.env.GITHUB_REPOSITORY = ""; 23 | process.env.GITHUB_SHA = ""; 24 | 25 | // MOCK 26 | nock("https://api.github.com") 27 | // get changed files 28 | .get( 29 | "/repos/twitter-together/action/compare/0000000000000000000000000000000000000001...0000000000000000000000000000000000000002" 30 | ) 31 | .reply(500); 32 | 33 | process.on("exit", (code) => { 34 | tap.equal(code, 1); 35 | tap.same(nock.pendingMocks(), []); 36 | 37 | // above code exits with 1 (error), but tap expects 0. 38 | // Tap adds the "process.exitCode" property for that purpose. 39 | process.exitCode = 0; 40 | }); 41 | 42 | require("../../lib"); 43 | -------------------------------------------------------------------------------- /test/push-main-request-error/tweets/hello-world.tweet: -------------------------------------------------------------------------------- 1 | Hello, world! 2 | -------------------------------------------------------------------------------- /test/push-main-setup-pending/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/main", 3 | "before": "0000000000000000000000000000000000000001", 4 | "after": "0000000000000000000000000000000000000002", 5 | "head_commit": { 6 | "id": "0000000000000000000000000000000000000002" 7 | }, 8 | "repository": { 9 | "owner": { 10 | "login": "twitter-together" 11 | }, 12 | "name": "action", 13 | "default_branch": "main" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/push-main-setup-pending/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the setup routine that occurs on a push to main 3 | * when the `tweets/` folder does not yet exist, but there is a pending 4 | * pull request adding it already 5 | */ 6 | 7 | const path = require("path"); 8 | 9 | const nock = require("nock"); 10 | const tap = require("tap"); 11 | 12 | // SETUP 13 | process.env.GITHUB_EVENT_NAME = "push"; 14 | process.env.GITHUB_TOKEN = "secret123"; 15 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 16 | process.env.GITHUB_REF = "refs/heads/main"; 17 | process.env.GITHUB_WORKSPACE = path.dirname(process.env.GITHUB_EVENT_PATH); 18 | 19 | // set other env variables so action-toolkit is happy 20 | process.env.GITHUB_WORKFLOW = ""; 21 | process.env.GITHUB_ACTION = "twitter-together"; 22 | process.env.GITHUB_ACTOR = ""; 23 | process.env.GITHUB_REPOSITORY = ""; 24 | process.env.GITHUB_SHA = ""; 25 | 26 | // MOCK 27 | nock("https://api.github.com", { 28 | reqheaders: { 29 | authorization: "token secret123", 30 | }, 31 | }) 32 | // check if twitter-together-setup branch exists 33 | .head( 34 | "/repos/twitter-together/action/git/refs/heads%2Ftwitter-together-setup" 35 | ) 36 | .reply(200); 37 | 38 | process.on("exit", (code) => { 39 | tap.equal(code, 0); 40 | tap.same(nock.pendingMocks(), []); 41 | 42 | // for some reason, tap fails with "Suites: 1 failed" if we don't exit explicitly 43 | process.exit(0); 44 | }); 45 | 46 | require("../../lib"); 47 | -------------------------------------------------------------------------------- /test/push-main-setup/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/main", 3 | "before": "0000000000000000000000000000000000000001", 4 | "after": "0000000000000000000000000000000000000002", 5 | "head_commit": { 6 | "id": "0000000000000000000000000000000000000002" 7 | }, 8 | "repository": { 9 | "owner": { 10 | "login": "twitter-together" 11 | }, 12 | "name": "action", 13 | "default_branch": "main" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/push-main-setup/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the setup routine that occurs on a push to main 3 | * when the `tweets/` folder does not yet exist 4 | */ 5 | 6 | const path = require("path"); 7 | 8 | const nock = require("nock"); 9 | const tap = require("tap"); 10 | 11 | // SETUP 12 | process.env.GITHUB_EVENT_NAME = "push"; 13 | process.env.GITHUB_TOKEN = "secret123"; 14 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 15 | process.env.GITHUB_REF = "refs/heads/main"; 16 | process.env.GITHUB_WORKSPACE = path.dirname(process.env.GITHUB_EVENT_PATH); 17 | process.env.GITHUB_SHA = "0000000000000000000000000000000000000002"; 18 | 19 | // set other env variables so action-toolkit is happy 20 | process.env.GITHUB_WORKFLOW = ""; 21 | process.env.GITHUB_ACTION = "twitter-together"; 22 | process.env.GITHUB_ACTOR = ""; 23 | process.env.GITHUB_REPOSITORY = ""; 24 | 25 | // MOCK 26 | nock("https://api.github.com", { 27 | reqheaders: { 28 | authorization: "token secret123", 29 | }, 30 | }) 31 | // check if twitter-together-setup branch exists 32 | .head( 33 | "/repos/twitter-together/action/git/refs/heads%2Ftwitter-together-setup" 34 | ) 35 | .reply(404) 36 | 37 | // Create the "twitter-together-setup" branch 38 | .post("/repos/twitter-together/action/git/refs", (body) => { 39 | tap.equal(body.ref, "refs/heads/twitter-together-setup"); 40 | tap.equal(body.sha, "0000000000000000000000000000000000000002"); 41 | 42 | return true; 43 | }) 44 | .reply(201) 45 | 46 | // Read contents of tweets/README.md file in twitter-together/action 47 | .get("/repos/twitter-together/action/contents/tweets%2FREADME.md") 48 | .reply(200, "contents of tweets/README.md") 49 | 50 | // Create tweets/README.md file 51 | .put("/repos/twitter-together/action/contents/tweets%2FREADME.md", (body) => { 52 | tap.equal( 53 | body.content, 54 | Buffer.from("contents of tweets/README.md").toString("base64") 55 | ); 56 | tap.equal(body.branch, "twitter-together-setup"); 57 | tap.equal(body.message, "twitter-together setup"); 58 | 59 | return true; 60 | }) 61 | .reply(201) 62 | 63 | // Create pull request 64 | .post("/repos/twitter-together/action/pulls", (body) => { 65 | tap.equal(body.title, "🐦 twitter-together setup"); 66 | tap.match( 67 | body.body, 68 | /This pull request creates the `tweets\/` folder where your `\*\.tweet` files go into/ 69 | ); 70 | tap.equal(body.head, "twitter-together-setup"); 71 | tap.equal(body.base, "main"); 72 | 73 | return true; 74 | }) 75 | .reply(201, { 76 | html_url: "https://github.com/twitter-together/action/pull/123", 77 | }); 78 | 79 | process.on("exit", (code) => { 80 | tap.equal(code, 0); 81 | tap.same(nock.pendingMocks(), []); 82 | }); 83 | 84 | require("../../lib"); 85 | -------------------------------------------------------------------------------- /test/push-main-tweet-error/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/main", 3 | "before": "0000000000000000000000000000000000000001", 4 | "after": "0000000000000000000000000000000000000002", 5 | "head_commit": { 6 | "id": "0000000000000000000000000000000000000002" 7 | }, 8 | "repository": { 9 | "owner": { 10 | "login": "twitter-together" 11 | }, 12 | "name": "action", 13 | "default_branch": "main" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/push-main-tweet-error/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of a commit to the main branch (main) 3 | * which includes a new *.tweet file. 4 | */ 5 | 6 | const path = require("path"); 7 | 8 | const nock = require("nock"); 9 | const tap = require("tap"); 10 | 11 | // SETUP 12 | process.env.GITHUB_EVENT_NAME = "push"; 13 | process.env.GITHUB_TOKEN = "secret123"; 14 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 15 | process.env.GITHUB_REF = "refs/heads/main"; 16 | process.env.GITHUB_WORKSPACE = path.dirname(process.env.GITHUB_EVENT_PATH); 17 | 18 | // set other env variables so action-toolkit is happy 19 | process.env.GITHUB_WORKFLOW = ""; 20 | process.env.GITHUB_ACTION = "twitter-together"; 21 | process.env.GITHUB_ACTOR = ""; 22 | process.env.GITHUB_REPOSITORY = ""; 23 | process.env.GITHUB_SHA = ""; 24 | 25 | // MOCK 26 | nock("https://api.github.com", { 27 | reqheaders: { 28 | authorization: "token secret123", 29 | }, 30 | }) 31 | // get changed files 32 | .get( 33 | "/repos/twitter-together/action/compare/0000000000000000000000000000000000000001...0000000000000000000000000000000000000002" 34 | ) 35 | .reply(200, { 36 | files: [ 37 | { 38 | status: "added", 39 | filename: "tweets/cupcake-ipsum.tweet", 40 | }, 41 | ], 42 | }) 43 | 44 | // post comment 45 | .post( 46 | "/repos/twitter-together/action/commits/0000000000000000000000000000000000000002/comments", 47 | (body) => { 48 | console.log(body.body); 49 | tap.equal( 50 | body.body, 51 | "Errors:\n\n- Tweet exceeds maximum length of 280 characters by 166 characters" 52 | ); 53 | return true; 54 | } 55 | ) 56 | .reply(201); 57 | 58 | process.on("exit", (code) => { 59 | tap.equal(code, 1); 60 | tap.same(nock.pendingMocks(), []); 61 | 62 | // above code exits with 1 (error), but tap expects 0. 63 | // Tap adds the "process.exitCode" property for that purpose. 64 | process.exitCode = 0; 65 | }); 66 | 67 | require("../../lib"); 68 | -------------------------------------------------------------------------------- /test/push-main-tweet-error/tweets/cupcake-ipsum.tweet: -------------------------------------------------------------------------------- 1 | Cupcake ipsum dolor sit amet chupa chups candy halvah I love. Apple pie gummi bears chupa chups jujubes I love cake jelly. Jelly candy canes pudding jujubes caramels sweet roll I love. Sweet fruitcake oat cake I love brownie sesame snaps apple pie lollipop. 2 | 3 | Pie dragée I love apple pie cotton candy candy chocolate bar. Candy gummi bears fruitcake wafer. Chocolate bar sweet roll lemon drops. Icing topping fruitcake lollipop chupa chups I love. 4 | -------------------------------------------------------------------------------- /test/push-not-a-branch/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/tags/v1.0.0" 3 | } 4 | -------------------------------------------------------------------------------- /test/push-not-a-branch/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of a commit to the main branch (main) 3 | * which includes a new *.tweet file. 4 | */ 5 | 6 | const tap = require("tap"); 7 | 8 | // SETUP 9 | process.env.GITHUB_EVENT_NAME = "push"; 10 | process.env.GITHUB_REF = "refs/tags/v1.0.0"; 11 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 12 | process.env.GITHUB_TOKEN = "secret123"; 13 | 14 | // set other env variables so action-toolkit is happy 15 | process.env.GITHUB_WORKSPACE = ""; 16 | process.env.GITHUB_WORKFLOW = ""; 17 | process.env.GITHUB_ACTION = "twitter-together"; 18 | process.env.GITHUB_ACTOR = ""; 19 | process.env.GITHUB_REPOSITORY = ""; 20 | process.env.GITHUB_SHA = ""; 21 | 22 | process.on("exit", (code) => { 23 | tap.equal(code, 0); 24 | 25 | // for some reason, tap fails with "Suites: 1 failed" if we don't exit explicitly 26 | process.exit(0); 27 | }); 28 | 29 | require("../../lib"); 30 | -------------------------------------------------------------------------------- /test/push-not-default-branch/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/tags/v1.0.0", 3 | "repository": { 4 | "default_branch": "main" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/push-not-default-branch/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This test checks the happy path of a commit to the main branch (main) 3 | * which includes a new *.tweet file. 4 | */ 5 | 6 | const tap = require("tap"); 7 | 8 | // SETUP 9 | process.env.GITHUB_EVENT_NAME = "push"; 10 | process.env.GITHUB_REF = "refs/heads/patch"; 11 | process.env.GITHUB_EVENT_PATH = require.resolve("./event.json"); 12 | process.env.GITHUB_TOKEN = "secret123"; 13 | 14 | // set other env variables so action-toolkit is happy 15 | process.env.GITHUB_WORKSPACE = ""; 16 | process.env.GITHUB_WORKFLOW = ""; 17 | process.env.GITHUB_ACTION = "twitter-together"; 18 | process.env.GITHUB_ACTOR = ""; 19 | process.env.GITHUB_REPOSITORY = ""; 20 | process.env.GITHUB_SHA = ""; 21 | 22 | process.on("exit", (code) => { 23 | tap.equal(code, 0); 24 | 25 | // for some reason, tap fails with "Suites: 1 failed" if we don't exit explicitly 26 | process.exit(0); 27 | }); 28 | 29 | require("../../lib"); 30 | -------------------------------------------------------------------------------- /tweets/2019/02/twitter-together.tweet: -------------------------------------------------------------------------------- 1 | Twitter, together! 2 | -------------------------------------------------------------------------------- /tweets/2019/03/Can I do this from here.tweet: -------------------------------------------------------------------------------- 1 | Tweeting through the GitHub UI. @RichLitt 2 | 3 | -------------------------------------------------------------------------------- /tweets/2019/03/nock.tweet: -------------------------------------------------------------------------------- 1 | 🦉 @nodenock now twitters together: 2 | https://twitter.com/nodenock/status/1105499577224912896 3 | -------------------------------------------------------------------------------- /tweets/2019/03/probot.tweet: -------------------------------------------------------------------------------- 1 | 👋 @ProbotTheRobot now twitters together: 2 | https://twitter.com/ProbotTheRobot/status/1105154423410454528 3 | -------------------------------------------------------------------------------- /tweets/2019/happy-new-year.tweet: -------------------------------------------------------------------------------- 1 | 🥳 Happy new year! 2 | 3 | This tweet was scheduled using https://github.com/gr2m/merge-schedule-action 4 | -------------------------------------------------------------------------------- /tweets/2019/v2.tweet: -------------------------------------------------------------------------------- 1 | G'bye HCL-based @GitHub Actions: https://github.blog/changelog/2019-10-01-github-actions-hcl-workflows-are-no-longer-being-run/ 2 | 3 | I just migrated my own actions, it was fun: https://github.com/twitter-together/action/pull/73/files 4 | -------------------------------------------------------------------------------- /tweets/2020/twitter-api-v7.tweet: -------------------------------------------------------------------------------- 1 | Ohaj there @TwitterAPI v7! Thank you @Jolg42 for upgrading me 👋🤖 2 | -------------------------------------------------------------------------------- /tweets/2020/twitter-api-v8.tweet: -------------------------------------------------------------------------------- 1 | Now using @TwitterAPI v8 🤖🎉 2 | -------------------------------------------------------------------------------- /tweets/2021/look-pops-we-made-it-on-github.tweet: -------------------------------------------------------------------------------- 1 | Look, pops! We made it! 2 | 3 | https://twitter.com/github/status/1461803226890653697 4 | -------------------------------------------------------------------------------- /tweets/2021/sample.tweet: -------------------------------------------------------------------------------- 1 | This is a sample tweet. @nishantbangarwa #test 2 | -------------------------------------------------------------------------------- /tweets/README.md: -------------------------------------------------------------------------------- 1 | # The tweets/ folder 2 | 3 | To create a new tweet create a new `*.tweet` file in this `tweets/` folder. 4 | 5 | [Create new tweet](../../../new/main/?filename=tweets/.tweet) 6 | 7 | ## Example 8 | 9 | Create a new file `tweets/hello-world.tweet` with the content 10 | 11 | > Hello, world! 12 | 13 | You can use subfolders, e.g. `tweets/2019-02/hello-world.tweet`, as long as the file is in the `tweets/` folder and has the `.tweet` file extension 14 | 15 | ## Create a tweet with a twitter poll 16 | 17 | **Note**: The configured twitter account needs to be authorized to use Twitter's Ads API in order to send tweets including a poll. 18 | 19 | A tweet including a poll must end with 2-4 options in the following format 20 | 21 | > Here is some text 22 | > 23 | > ( ) option A 24 | > ( ) option B 25 | > ( ) option C 26 | > ( ) option D 27 | 28 | ## Notes 29 | 30 | - Only newly created files are handled, deletions, updates or renames are ignored. 31 | - `*.tweet` files will not be created for tweets you send out directly from twitter.com 32 | - If you need to rename an existing tweet file, please do so locally using [`git mv old_filename new_filename`](https://help.github.com/en/articles/renaming-a-file-using-the-command-line), otherwise it may occur as deleted and added which would trigger a new tweet. 33 | - your message must fit into a single tweet 34 | 35 | ## Questions? 36 | 37 | If you have any further questions or suggestions, please create an issue at https://github.com/twitter-together/action/issues/new 38 | -------------------------------------------------------------------------------- /tweets/ag-poll.tweet: -------------------------------------------------------------------------------- 1 | crees que hacer tweets desde Github es genial? 2 | 3 | ( ) CLARO ❤️❤️ 4 | ( ) POR SU PUESTO 👏 5 | ( ) BRUTAL 🤯!! 6 | -------------------------------------------------------------------------------- /tweets/hello-from-si.tweet: -------------------------------------------------------------------------------- 1 | Keen to see how this works. A great idea for sharing a Twitter account with a developer approach. Say hello @Si. 2 | -------------------------------------------------------------------------------- /tweets/hello-world-horacioh.tweet: -------------------------------------------------------------------------------- 1 | Hello World! 🤯 2 | -------------------------------------------------------------------------------- /tweets/hello-world.tweet: -------------------------------------------------------------------------------- 1 | Hello, world! 2 | -------------------------------------------------------------------------------- /tweets/johnb-test.tweet: -------------------------------------------------------------------------------- 1 | Hello, World! @Bin_fluence 2 | -------------------------------------------------------------------------------- /tweets/kati-tweet.tweet: -------------------------------------------------------------------------------- 1 | Hello, world! @KatiMichel 2 | -------------------------------------------------------------------------------- /tweets/ktweet-test.tweet: -------------------------------------------------------------------------------- 1 | Hello, World! 2 | -------------------------------------------------------------------------------- /tweets/my-first.tweet: -------------------------------------------------------------------------------- 1 | What's your favorite pet? 2 | 3 | ( ) Dog 4 | ( ) Cat 5 | ( ) Bird 6 | ( ) Bunny 7 | -------------------------------------------------------------------------------- /tweets/poll-take-5.tweet: -------------------------------------------------------------------------------- 1 | Look, ma! A poll! 2 | 3 | ( ) option A 4 | ( ) option B 5 | ( ) option C 6 | ( ) option D 7 | -------------------------------------------------------------------------------- /tweets/poll-take-6.tweet: -------------------------------------------------------------------------------- 1 | Look, ma! A poll! 2 | 3 | ( ) option A 4 | ( ) option B 5 | ( ) option C 6 | ( ) option D 7 | -------------------------------------------------------------------------------- /tweets/poll-take-7.tweet: -------------------------------------------------------------------------------- 1 | Look, ma! A poll! 2 | 3 | ( ) option A 4 | ( ) option B 5 | ( ) option C 6 | ( ) option D 7 | -------------------------------------------------------------------------------- /tweets/poll.tweet: -------------------------------------------------------------------------------- 1 | Look, ma! A poll! 2 | 3 | ( ) option A 4 | ( ) option B 5 | ( ) option C 6 | ( ) option D 7 | -------------------------------------------------------------------------------- /tweets/prim4t.tweet: -------------------------------------------------------------------------------- 1 | Dude, this is magic! 2 | This tweet is generated by a pull request on @github... 3 | 4 | #github 5 | -------------------------------------------------------------------------------- /tweets/self_ref.tweet: -------------------------------------------------------------------------------- 1 | A self-referential tweet made by @sinabooeshaghi with @commit2tweet! https://github.com/twitter-together/action/blob/master/tweets/self_ref.tweet 2 | -------------------------------------------------------------------------------- /tweets/test-davide.tweet: -------------------------------------------------------------------------------- 1 | Complete the sentence: 2 | «You should follow @BelloneDavide because he ____» 3 | -------------------------------------------------------------------------------- /tweets/test/debugging-squash-merge.tweet: -------------------------------------------------------------------------------- 1 | Debugging a problem with squash merges 2 | -------------------------------------------------------------------------------- /tweets/test/does-this-still-work.tweet: -------------------------------------------------------------------------------- 1 | Hello, hello, does this still work? 2 | -------------------------------------------------------------------------------- /tweets/test/fork.tweet: -------------------------------------------------------------------------------- 1 | May the forks be with us! 2 | -------------------------------------------------------------------------------- /tweets/test/hello-world.tweet: -------------------------------------------------------------------------------- 1 | Twitter, together. 2 | -------------------------------------------------------------------------------- /tweets/test/multiline.tweet: -------------------------------------------------------------------------------- 1 | This is a tweet 2 | with line breaks 3 | 4 | and paragraphs ❡🧚🏽‍♂️ 5 | -------------------------------------------------------------------------------- /tweets/test/progress.tweet: -------------------------------------------------------------------------------- 1 | I have a sweet setup now, and comments with URLs to ma tweets 2 | -------------------------------------------------------------------------------- /tweets/test/username-and-hashtag.tweet: -------------------------------------------------------------------------------- 1 | This is @gr2m’s #firsttweet, sorta. 2 | --------------------------------------------------------------------------------