turning-issues-into-posts.mdx
1 --- 2 title: Turning issues into posts 3 date: 2025-04-10 4 description: Github issues combined with a simple workflow script to manage contents 5 tags: 6 - website 7 --- 8 9 If you don't know already, I'm using [Astro](https://astro.build/) to build this website. The code is hosted publicly at [elianiva/elianiva.my.id](https://github.com/elianiva/elianiva.my.id) if you want to check it out. 10 11 The posts are written in [markdown (.mdx) files](https://github.com/elianiva/elianiva.my.id/blob/master/src/content/posts/) so if I want to add a new post, I have to create a new file, make a commit, and then push it to trigger the deployment. Unlike a traditional CMS where you have this fancy dashboard that you can use, this one is pretty barebones, and I like it this way. It allows me to use any text editors I want to edit the contents--oh, I use [Neovim](https://neovim.io), btw. 12 13 Now, the problem is that not all of my posts are long-form. I have a few bookmarks / TILs. They're basically just small notes that don't warrant a full blog post. 14 15 In case you don't know what that is, here are a few examples from other people: 16 17 - https://www.visualmode.dev/a-decade-of-tils 18 - https://simonwillison.net/2021/May/2/one-year-of-tils/ 19 - https://github.com/jbranchaud/til 20 - https://blog.reinaldyrafli.com/til 21 22 Oh, wanna see mine? Try hitting the yellow square at the top left corner ;) 23 24 ## When it starts to get painful 25 26 Like I said before, the contents of this website are all in markdown files and it involves me going through several steps to publish contents in it. If I'm using my laptop then sure it's easy to do, but when I'm on mobile it's such a hassle. I have to set up Termux, git clone--not to mention having to set up my git credentials as well--write the markdown file, make a commit, and then push. That's too much work for my lazy ass. 27 28 Sometimes, I just want to jot down a quick note and save it. I'm not always in front of my laptop, so I need some easy way to do this from my phone without having to make a dedicated platform myself--which, I almost did. 29 30 ## Down into the rabbit hole 31 32 I've been trying several note-taking apps in the past. I tried [Notion](https://notion.so), but it didn't end very well because I ended up overengineering how it works instead of just taking notes. I also tried [Obsidian](https://obsidian.md), which is a bit better, but again, I got sucked into tweaking it too much. 33 34 I mean, Neovim itself is already a rabbit hole, but I've come to a point where I no longer do too many things with it. I've had my fair share of going through that rabbit hole already in the past. Now I just have a solid config and _actually use_ the editor. 35 36 ## The solution 37 38 I had a discussion on Discord the other day on how should we approach this. I was thinking of just using a separate platform that has the feature built-in, such as Obsidian which has [Obsidian Publish](https://obsidian.md/publish). There's also Notion. 39 40 Although they are simple to use, they're still too overkill for this simple task. Each TIL item is just a short-form text, mostly 1-2 paragraphs, maybe a few links here and there. If you're already using Obsidian or Notion then yeah, this is the easiest option. For those who prefer rolling stuff out by themself, it's too much. 41 42 Another option is to use email. You can just send an email to a dedicated account which will then get picked up, do the thing, and so on and so forth. It could work, but email? Really? Nah, that ain't zoomer enough. 43 44 Finally, there's Github issues. This is great because for one, it's free and it's integrated into the platform that I'm already using to host the code, and two, it's relatively easy to automate stuff on Github. You can just _do_ things. 45 46 ## Github Issues came in clutch 47 48 The idea is to use Github Issues to manage the content. Since the contents are markdown files and Github supports markdown syntax, it's pretty easy to start writing, press submit, and let the Github Action take over and do the tedious thing for you. 49 50 I didn't come up with this solution, it was [aldy505](https://github.com/aldy505) who did it first and I just adjusted it to fit my use case better. My version is a bit different because I need to also handle `bookmark` which is basically just some links I found useful. 51 52 Here's a simple workflow that he uses, which might fit your use case better because it's simple and you can build upon it. 53 54 ```yaml 55 name: Issue to TIL 56 57 on: 58 issues: 59 types: [opened] 60 61 concurrency: 62 group: ${{ github.workflow }}-${{ github.ref }} 63 cancel-in-progress: false 64 65 jobs: 66 build: 67 runs-on: ubuntu-latest 68 permissions: # Job-level permissions configuration starts here 69 contents: write # 'write' access to repository contents 70 pull-requests: write # 'write' access to pull requests 71 steps: 72 - uses: actions/checkout@v4 73 - uses: actions/setup-node@v4 74 with: 75 node-version: 22 76 - name: Install dependencies 77 run: npm install 78 - name: Install ULID 79 run: npm install ulid 80 - name: Grab the issue content body 81 uses: actions/github-script@v7 82 with: 83 retries: 3 84 script: | 85 const { ulid } = require('ulid'); 86 const fs = require('fs'); 87 88 const issue = { 89 body: "${{ github.event.issue.body }}", 90 }; 91 92 const id = ulid(); 93 const date = new Date().toISOString(); 94 95 fs.writeFileSync(`./src/content/tils/${id}.md`, `--- 96 id: ${id} 97 date: '${date}' 98 --- 99 ${issue.body}`); 100 - name: Create a commit 101 run: | 102 git config --local user.email "action@github.com" 103 git config --local user.name "GitHub Action" 104 git add . 105 git commit -m "Add TIL from #${{ github.event.issue.number }}" 106 - name: Push to repository 107 uses: ad-m/github-push-action@master 108 with: 109 github_token: ${{ github.token }} 110 branch: master 111 - name: Close Issue 112 run: gh issue close --comment "Auto-closing issue" "${{ github.event.issue.number }}" 113 env: 114 GH_TOKEN: ${{ github.token }} 115 ``` 116 <small class="block opacity-70 -mt-4 mb-4">Yeah, he vibe coded this, that's why it looks a bit dodgy, he probably improved it or something in his repo but this is the one I took</small> 117 118 Basically, it does the following: 119 120 1. Grab the issue content 121 2. Write the content in `src/content/tils` with a unique ID as the filename 122 3. Create a commit with the new file 123 4. Push the commit to the repository 124 5. Close the issue 125 126 I basically just stole it :p -- and adjusted it to also handle a few different things. 127 128 ### Checks for author 129 130 Since his repository is private, he doesn't have to care about people creating issues and then accidentally publish it. My repo, on the other hand, is public. So I need a way to limit it to myself. 131 132 It's pretty simple to do, I just have to add this one line for the job section. 133 134 ```yaml 135 if: github.actor == github.repository_owner 136 ```` 137 138 It checks if the actor--the person who created this issue--is the same as the owner of the repository. If it's the same, then it will run the job. Otherwise, it won't. Pretty straight forward. 139 140 ### Handling different types 141 142 My version needs to handle both TIL and bookmark, so I need to adjust it a bit. I finally came up with this. 143 144 ```javascript 145 const issue = context.payload.issue; 146 if (!issue) { 147 core.setFailed("Could not get issue details"); 148 return; 149 } 150 151 const date = new Date().toISOString().split("T")[0]; 152 const title = issue.title; 153 const body = issue.body || ""; 154 const type = issue.labels?.find(label => label.name.startsWith("type:"))?.name.split(":")[1] || ""; 155 156 if (body.length === 0) { 157 core.setFailed("Issue body is empty"); 158 return; 159 } 160 161 if (type.length === 0) { 162 core.setFailed("Issue labels do not contain a type"); 163 return; 164 } 165 ``` 166 167 This part prepares the issue information that I need. I mark the issue as TIL or bookmark based on the issue label. If it has a label that starts with `type:` then it will be used. The rest are just handling some edge cases 168 169 ```javascript 170 const [content, links] = body.split("---").map(part => part.trim()); 171 const linksArray = links ? links.split("\n").map(link => link.trim()) : []; 172 173 const markdown = `---\n` + 174 `title: ${title}\n` + 175 `date: ${date}\n` + 176 `type: ${type}\n` + 177 (linksArray.length > 0 ? `links:\n${linksArray.map(link => ` - url: ${link}`).join("\n")}\n` : '') + 178 `---\n\n${content}\n`; 179 ``` 180 181 This part just prepares the markdown file. The reason why I split the content by `---` is because I have a dedicated `links` field so that I can display them on their own container. 182 183 An example issue content would look something like this: 184 185 ```markdown 186 This is the content body, nothing too special about it. 187 --- 188 https://elianiva.my.id/posts/today-i-learned 189 ``` 190 191 It will take the first part as the content body and parse the second part as an array of links. 192 193 ```javascript 194 const snakeCasedTitle = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""); 195 const filePath = path.join('${{ github.workspace }}', "src", "content", "bookmarks", `${snakeCasedTitle}.mdx`); 196 197 fs.writeFileSync(filePath, markdown); 198 console.log(`Successfully created TIL file: ${filePath}`); 199 ``` 200 201 This last part is basically writing the issue content to the file. I converted the issue title into snake-case and then used it for the file name to match the other files I already have. 202 203 That's pretty much the adjustment I made, the rest is just changing the file name, commit message, etc. 204 205 ### Putting it all together 206 207 Now, putting them all together, it should look something like this: 208 209 ```yaml 210 name: Create TIL/Bookmark from Issue 211 212 on: 213 issues: 214 types: [opened] 215 216 concurrency: 217 group: ${{ github.workflow }}-${{ github.ref }} 218 cancel-in-progress: false 219 220 jobs: 221 publish: 222 if: github.actor == github.repository_owner 223 runs-on: ubuntu-latest 224 permissions: 225 contents: write 226 issues: write 227 steps: 228 - name: Checkout repository 229 uses: actions/checkout@v4 230 231 - name: Create TIL markdown file from issue 232 uses: actions/github-script@v7 233 id: create-til 234 with: 235 retries: 3 236 script: | 237 const fs = require("fs"); 238 const path = require("path"); 239 240 const issue = context.payload.issue; 241 if (!issue) { 242 core.setFailed("Could not get issue details"); 243 return; 244 } 245 246 const date = new Date().toISOString().split("T")[0]; 247 const title = issue.title; 248 const body = issue.body || ""; 249 const type = issue.labels?.find(label => label.name.startsWith("type:"))?.name.split(":")[1] || ""; 250 251 if (body.length === 0) { 252 core.setFailed("Issue body is empty"); 253 return; 254 } 255 256 if (type.length === 0) { 257 core.setFailed("Issue labels do not contain a type"); 258 return; 259 } 260 261 const [content, links] = body.split("---").map(part => part.trim()); 262 const linksArray = links ? links.split("\n").map(link => link.trim()) : []; 263 264 const markdown = `---\n` + 265 `title: ${title}\n` + 266 `date: ${date}\n` + 267 `type: ${type}\n` + 268 (linksArray.length > 0 ? `links:\n${linksArray.map(link => ` - url: ${link}`).join("\n")}\n` : '') + 269 `---\n\n${content}\n`; 270 271 const snakeCasedTitle = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""); 272 const filePath = path.join('${{ github.workspace }}', "src", "content", "bookmarks", `${snakeCasedTitle}.mdx`); 273 274 fs.writeFileSync(filePath, markdown); 275 console.log(`Successfully created TIL file: ${filePath}`); 276 277 - name: Commit and push TIL file 278 run: | 279 git config --local user.email "action@github.com" 280 git config --local user.name "GitHub Action" 281 git add ./src/content/bookmarks/*.mdx 282 if git diff --staged --quiet; then 283 echo "No changes to commit." 284 else 285 git commit -m "bookmark/til: add content from #${{ github.event.issue.number }} [automated]" 286 echo "Commit created." 287 fi 288 289 - name: Push changes to repository 290 uses: ad-m/github-push-action@master 291 with: 292 github_token: ${{ secrets.GITHUB_TOKEN }} 293 branch: ${{ github.ref_name }} 294 295 - name: Close Issue 296 if: success() 297 run: gh issue close "${{ github.event.issue.number }}" --comment "Bookmark/TIL created and pushed. Auto-closing issue." 298 env: 299 GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 300 ``` 301 302 Honestly, I don't really like the way it looks because there's a piece of Javascript code shoved _inside_ YAML file. That's such a nightmare to work with. 303 304 Initially, I wanted to separate it into its own file--docs says you can--but kept getting errors about `module is not defined` or something like that :p 305 306 [fail-1]: https://github.com/elianiva/elianiva.my.id/issues/42 307 [fail-2]: https://github.com/elianiva/elianiva.my.id/issues/43 308 [fail-3]: https://github.com/elianiva/elianiva.my.id/issues/44 309 [fail-4]: https://github.com/elianiva/elianiva.my.id/issues/45 310 [fail-5]: https://github.com/elianiva/elianiva.my.id/issues/46 311 312 It actually [took][fail-1] [me][fail-2] [5][fail-3] [failed][fail-4] [attempts][fail-5] before finally got it working. I just gave up and inline the script to the YAML file. It works, and I'm not going to touch it anyway once I get it working. 313 314 ## Called it a wrap 315 316 There's not much to it, really. It's pretty simple and it works really well. Now I can just submit contents from my phone by creating a Github issue. 317 318 I didn't handle a lot of edge cases. For example, what would happen when I re-open the issue or something like that. I find it useless because I'm the only one who's going to use it, and if I ever needed something like that I can just do it manually :p 319 320 Anyway, if you want to do something similar, here are some links that might help you get there. 321 322 - [Github Actions Documentation](https://docs.github.com/en/actions) 323 - [Github Actions Script Documentation](https://github.com/actions/github-script) 324 - [My Workflow File](https://github.com/elianiva/elianiva.my.id/blob/master/.github/workflows/bookmark-til.yml) 325 326 There are maybe a few missing stuff that I don't really talk about in this post, in that case, feel free to leave a comment :) 327 328 Until next time! 👋