๋ฐ˜์‘ํ˜•

GitHub์—์„œ ์‚ฌ์šฉ์ž ์ด๋ฆ„(username)๊ณผ ๋™์ผํ•œ ์ด๋ฆ„์œผ๋กœ public ๋ ˆํฌ์ง€ํ† ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  RRADME.md ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ•˜๋ฉด ํ”„๋กœํ•„ ํŽ˜์ด์ง€ ์ƒ๋‹จ์— README ๋‚ด์šฉ์ด ํ‘œ์‹œ๋œ๋‹ค. ํ”„๋กœํ•„ ํŽ˜์ด์ง€์—” ์ฃผ๋กœ ์ž์‹ ์ด ์†Œ๊ฐœํ•˜๊ณ  ์‹ถ์€ ํ”„๋กœ์ ํŠธ, ๊ธฐ์ˆ  ์Šคํƒ ๋“ฑ์„ ๊ธฐ์žฌํ•œ๋‹ค. ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„๋กœ ์ฃผ๋ชฉํ• ๋งŒํ•œ ํ”„๋กœํ•„ ํŽ˜์ด์ง€๋ฅผ ๋ชจ์•„๋‘” ์‚ฌ์ดํŠธ๋„ ์žˆ๋‹ค.

 

@mokkapps

๋Œ€๋ถ€๋ถ„์˜ ๊ฐœ๋ฐœ์ž๋“ค์ด ๋ธ”๋กœ๊ทธ๋ฅผ ์šด์˜ํ•˜๊ณ  ์žˆ์–ด์„œ ๊ทธ๋Ÿฐ์ง€ Latest Blog Posts ํ—ค๋”์— ์ตœ๊ทผ ๊ธ€ ๋ชฉ๋ก์„ ๋ณด์—ฌ์ฃผ๋Š” ํ”„๋กœํ•„์ด ๋งŽ๋‹ค. ์ด ๊ธฐ๋Šฅ์€ rss-parser ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ†ตํ•ด RSS ํ”ผ๋“œ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ค๊ณ  README.md์— ๋ฐ˜์˜ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„๋˜๋Š”๋ฐ, ์ด ์ž‘์—…์„ GitHub Actions๋ฅผ ํ†ตํ•ด ์ •๊ธฐ์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•˜๋„๋ก ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

 

 

RSS ์„ค์ •


๋จผ์ € ์ž์‹ ์˜ ๋ธ”๋กœ๊ทธ ํ˜น์€ ์›น์‚ฌ์ดํŠธ์— RSS ์„ค์ •์„ ํ•ด์•ผ ํ•œ๋‹ค. RSS๋Š” ์›น์‚ฌ์ดํŠธ์— ์˜ฌ๋ผ์˜จ ์ •๋ณด๋ฅผ ๊ฐ„ํŽธํ•˜๊ฒŒ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•œ XML ๊ธฐ๋ฐ˜์˜ ํฌ๋งท์ด๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์›น์‚ฌ์ดํŠธ๋ฅผ ๋ฐฉ๋ฌธํ•˜์ง€ ์•Š์•„๋„ ์ตœ์‹  ์ฝ˜ํ…์ธ ๋ฅผ ์ž๋™์œผ๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ์žฅ์ ์ด ์žˆ๋‹ค.

 

๋Œ€๋ถ€๋ถ„์˜ ์ฝ˜ํ…์ธ  ์„œ๋น„์Šค๋Š” RSS๋ฅผ ์ง€์›ํ•œ๋‹ค. ํ‹ฐ์Šคํ† ๋ฆฌ ๋ธ”๋กœ๊ทธ๋Š” ๊ด€๋ฆฌ → ๋ธ”๋กœ๊ทธ → ๊ธฐํƒ€ ์„ค์ •์—์„œ RSS ๊ธฐ๋Šฅ์„ ์ผค ์ˆ˜ ์žˆ๋‹ค. RSS ํ”ผ๋“œ๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ๋„๋ฉ”์ธ ์ฃผ์†Œ ๋’ค์— /rss๋ฅผ ๋ถ™์ด๋Š” ๋ฐฉ์‹์œผ๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

 

ํŒŒ์‹ฑ ์Šคํฌ๋ฆฝํŠธ


๐Ÿ’ก ์•„๋ž˜ ๊ฐ€์ด๋“œ์—์„  pnpm ํŒจํ‚ค์ง€ ๋งค๋‹ˆ์ €๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

 

ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐํ™”

GitHub ํ”„๋กœํ•„ ๋ ˆํฌ์ง€ํ† ๋ฆฌ๋ฅผ ํด๋ก ํ•œ ํ›„ ์•„๋ž˜ ๋ช…๋ น์–ด๋กœ ํ”„๋กœ์ ํŠธ๋ฅผ ์ดˆ๊ธฐํ™”ํ•œ๋‹ค.

pnpm init -y

 

๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•˜๋ฉด package.json ํŒŒ์ผ์ด ์ƒ์„ฑ๋œ๋‹ค. Node.js๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ CommonJS(require ๊ตฌ๋ฌธ)์ด๋ฏ€๋กœ ์ต์ˆ™ํ•œ import ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก "type": "module"์„ ์ถ”๊ฐ€ํ•œ๋‹ค. ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰์„ ์œ„ํ•ด scripts ์†์„ฑ์— ๋ช…๋ น์–ด๋„ ๋“ฑ๋กํ•ด๋‘”๋‹ค.

// package.json

{
  name: "username",
  version: "1.0.0",
  description: "",
  main: "index.js",
  type: "module", // ์ถ”๊ฐ€
  scripts: {
    test: 'echo "Error: no test specified" && exit 1',
    "rss-to-readme": "node rss-to-readme.js", // ์ถ”๊ฐ€
  },
  keywords: [],
  author: "",
  license: "ISC",
};

 

RSS ํ”ผ๋“œ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ค๊ณ  ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜๊ธฐ ์œ„ํ•ด rss-parser ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•œ๋‹ค.

pnpm add rss-parser

 

์Šคํฌ๋ฆฝํŠธ ์ž‘์„ฑ

rss-to-readme.js ํŒŒ์ผ์— ์•„๋ž˜ ๋‚ด์šฉ์„ ๋ถ™์—ฌ๋„ฃ๊ณ  TODO ์ฃผ์„ ๋ถ€๋ถ„์„ ์ˆ˜์ •ํ•œ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ ์ตœ๊ทผ 5๊ฐœ ๊ธ€์„ ๋ถˆ๋Ÿฌ์™€์„œ Latest Blog Posts ํ—ค๋”(๋ ˆ๋ฒจ2) ์•„๋ž˜ ๋ถˆ๋ฆฟ ๋ชฉ๋ก์œผ๋กœ ์ถ”๊ฐ€ํ•œ๋‹ค. ๋ชฉ๋ก์€ ํ”„๋กœํ•„ ํŽ˜์ด์ง€ ๊ฐ€์žฅ ํ•˜๋‹จ์— ์œ„์น˜ํ•œ๋‹ค.

// rss-to-readme.js

import { readFileSync, writeFileSync } from 'node:fs';
import Parser from 'rss-parser';

// Constants
const README_PATH = './README.md';
const RSS_URL = 'https://romantech.net/rss'; // TODO - ๋ธ”๋กœ๊ทธ RSS ์ฃผ์†Œ
const RSS_HEADERS = { Accept: 'application/rss+xml, application/xml, text/xml; q=0.1' };

const MAX_POSTS = 5; // TODO - ํ‘œ์‹œํ•  ํฌ์ŠคํŒ… ๊ฐœ์ˆ˜
const HEADER_PREFIX = '##'; // TODO - ํ—ค๋” ๋ ˆ๋ฒจ
const POSTS_HEADER = `${HEADER_PREFIX} Latest Blog Posts`; // TODO - ํฌ์ŠคํŒ… ๋ชฉ๋ก ํ—ค๋” ์ด๋ฆ„
const POSTS_REGEX = new RegExp(`${POSTS_HEADER}[\\s\\S]*?(?=\\n${HEADER_PREFIX}|\\n$)`);

const parser = new Parser({ headers: RSS_HEADERS });

// RSS ํ”ผ๋“œ ๋ชฉ๋ก fetching
const getRSSFeed = async (url) => await parser.parseURL(url);

// URL์ด 'http://'๋กœ ์‹œ์ž‘ํ•˜๋ฉด 'https://'๋กœ ๋ณ€ํ™˜
const convertToHTTPS = (url) => {
  if (url.startsWith('http://')) return url.replace('http://', 'https://');
  return url;
};

// RSS ํ”ผ๋“œ ๋ชฉ๋ก์„ ๋งˆํฌ๋‹ค์šด ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜
const createPostsMarkdown = (items, count) => {
  const posts = items.slice(0, count).map(({ title, link }) => {
    return `- [${title}](${convertToHTTPS(link)})`;
  });
  return [POSTS_HEADER, ...posts].join('\n');
};

// README.md ํŒŒ์ผ ์—…๋ฐ์ดํŠธ
const updateReadme = (content, newPosts) => {
  const hasPosts = content.includes(POSTS_HEADER);
  if (hasPosts) return content.replace(POSTS_REGEX, newPosts + '\n');

  return content + '\n' + newPosts;
};

// ์œ„ 3๊ฐ€์ง€ ์ž‘์—…์„ ๋ชจ๋‘ ์ˆ˜ํ–‰ํ•˜๋Š” ํ•จ์ˆ˜
const refreshReadme = async () => {
  const readme = readFileSync(README_PATH, 'utf8');
  const feed = await getRSSFeed(RSS_URL);
  const newPosts = createPostsMarkdown(feed.items, MAX_POSTS);

  const updatedReadme = updateReadme(readme, newPosts);

  if (updatedReadme !== readme) {
    writeFileSync(README_PATH, updatedReadme, 'utf8');
    console.log('README.md updated successfully');
  } else {
    console.log('No new blog posts. README.md was not updated.');
  }
};

try {
  await refreshReadme();
} catch (error) {
  console.error('Error during the refresh process:', error);
  process.exit(1);
} finally {
  process.exit(0);
}

 

ํŒŒ์ผ ์ˆ˜์ •์„ ๋ชจ๋‘ ๋งˆ์น˜๊ณ  ํ„ฐ๋ฏธ๋„์— pnpm rss-to-readme ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•ด๋ณด์ž. README.md ํŒŒ์ผ์— ๊ฐ€์žฅ ์ตœ๊ทผ ์ž‘์„ฑํ•œ ๊ธ€ 5๊ฐœ๊ฐ€ ์ถ”๊ฐ€๋๋‹ค๋ฉด ์„ฑ๊ณต.

 

 

GitHub Actions ์›Œํฌํ”Œ๋กœ์šฐ


GitHub Actions๋Š” ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ ํ”„๋กœ์ ํŠธ์˜ ๋นŒ๋“œ, ํ…Œ์ŠคํŠธ, ๋ฐฐํฌ ๋“ฑ์„ ์ž๋™ํ™” ํ•˜๊ธฐ ์œ„ํ•œ CI/CD ์„œ๋น„์Šค๋‹ค. GitHub Actions๋ฅผ ์ด์šฉํ•ด ์ฃผ๊ธฐ์ ์œผ๋กœ rss-to-readme.js ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•˜๊ณ  README.md ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ž๋™์œผ๋กœ ์ปค๋ฐ‹ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

GitHub Actions ์›Œํฌํ”Œ๋กœ์šฐ๋Š” yml ํŒŒ์ผ์„ ํ†ตํ•ด์„œ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, yml ํŒŒ์ผ์€ ํ•ญ์ƒ workflows ํด๋”์— ์œ„์น˜ํ•ด์•ผ ํ•œ๋‹ค. ๋จผ์ € .github/workflows ํด๋”์— rss-to-readme.yml ํŒŒ์ผ์„ ์ƒ์„ฑํ•œ๋‹ค.

๐Ÿ“ฆ GitHub Profile Repository
โ””โ”€ .github
   โ””โ”€ workflows
      โ””โ”€ rss-to-readme.yml

 

์•„๋ž˜ ๋‚ด์šฉ์„ ๋ถ™์—ฌ ๋„ฃ์€ ํ›„ your@email.com ๋ถ€๋ถ„์„ GitHub์— ๋“ฑ๋กํ•œ ๋ฉ”์ผ ์ฃผ์†Œ๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ ๋งค ์‹œ๊ฐ„ ์ •๊ฐ๋งˆ๋‹ค ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์‹คํ–‰ํ•˜๋„๋ก ์ž‘์„ฑ๋ผ ์žˆ๋‹ค. ์›ํ•œ๋‹ค๋ฉด schedule: cron: ์†์„ฑ์„ ํ†ตํ•ด ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค. ์ฐธ๊ณ ๋กœ GitHub Actions๋Š” UTC ์‹œ๊ฐ„ ๊ธฐ์ค€์ด๋‹ค. ํŠน์ • ํ•œ๊ตญ ์‹œ๊ฐ„์œผ๋กœ ์„ค์ •ํ•˜๋ ค๋ฉด +9 ์‹œ๊ฐ„์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค.

# rss-to-readme.yml

name: RSS to README

on:
  schedule:
    - cron: "0 */1 * * *" # ๋งค ์‹œ๊ฐ„ ์ •๊ฐ์— ์‹คํ–‰ (UTC)
  push:
    branches:
      - main # ์ง€์ •ํ•œ ๋ธŒ๋žœ์น˜์— push ํ•  ๋•Œ ์›Œํฌํ”Œ๋กœ์šฐ ์‹คํ–‰
  workflow_dispatch: # ์ˆ˜๋™ ํŠธ๋ฆฌ๊ฑฐ ๊ธฐ๋Šฅ ํ™œ์„ฑ

jobs:
  update-readme:
    runs-on: ubuntu-latest
    timeout-minutes: 30 # ์›Œํฌํ”Œ๋กœ์šฐ๊ฐ€ 30๋ถ„ ๋‚ด์— ์™„๋ฃŒ๋˜์ง€ ์•Š์œผ๋ฉด ์ž๋™ ์ข…๋ฃŒ

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Install Node.js
        uses: actions/setup-node@v4
        with:
          node-version: lts/*

      - uses: pnpm/action-setup@v4
        name: Install pnpm and dependencies
        with:
          version: latest
          run_install: true

      - name: Update README
        run: pnpm rss-to-readme # ์—…๋ฐ์ดํŠธ ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰

      - name: Configure Git
        run: |
          git config --local user.name 'GitHub Action Bot'
          git config --local user.email 'your@email.com' # ์ˆ˜์ •

      - name: Commit and push if changed
        run: |
          # Git ์ €์žฅ์†Œ์— ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์žˆ์„ ๋•Œ๋งŒ ๋ช…๋ น์–ด ์‹คํ–‰
          if [ -n "$(git status --porcelain)" ]; then
            git add README.md
            git commit -m "Update README with latest articles [auto-generated]" -m "Generated at: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
            git push
          else
            echo "No changes to commit"
          fi

 

์œ„ ๋ณ€๊ฒฝ ๋‚ด์šฉ์„ ์ปค๋ฐ‹ํ•˜๊ณ  main ๋ธŒ๋žœ์น˜๋กœ push ํ•ด๋ณด์ž. push: branches: ์†์„ฑ์„ main์œผ๋กœ ์„ค์ •ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— push ์ด๋ ฅ์„ ๊ฐ์ง€ํ•ด์„œ ์›Œํฌํ”Œ๋กœ์šฐ๊ฐ€ ์ž๋™์œผ๋กœ ์‹คํ–‰๋œ๋‹ค. ์ž์„ธํ•œ ์‹คํ–‰ ๊ณผ์ •์€ Actions ํƒญ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•˜๋ฉด README.md ํŒŒ์ผ์„ ์ˆ˜์ •ํ•œ ์ปค๋ฐ‹์ด ์ถ”๊ฐ€๋œ๊ฑธ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

 

GPG Signatures


GPG Signatures๋Š” Git ์„œ๋ช… ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜๋กœ ์ปค๋ฐ‹์˜ ๋ฌด๊ฒฐ์„ฑ๊ณผ ์‹ ๋ขฐ์„ฑ์„ ๋ณด์žฅํ•˜๋Š” ๊ธฐ์ˆ ์ด๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์ปค๋ฐ‹์ด ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ์ถœ์ฒ˜์—์„œ ์ž‘์„ฑ๋˜์—ˆ์Œ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ์ปค๋ฐ‹ ๋ชฉ๋ก์— ์ดˆ๋ก์ƒ‰ Verified ๋ฐฐ์ง€๊ฐ€ ๋ถ™์œผ๋ฉด ์ปค๋ฐ‹์ด ์œ ํšจํ•œ GPG ํ‚ค๋กœ ์„œ๋ช…๋˜์—ˆ์Œ์„ ์˜๋ฏธํ•œ๋‹ค.

 

 

GitHub Actions๋ฅผ ํ†ตํ•œ ์ปค๋ฐ‹์€ ๊ธฐ๋ณธ์ ์œผ๋กœ GPG ์„œ๋ช…์ด ์ ์šฉ๋˜์–ด ์žˆ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋ณ„๋„ ์„ค์ •์ด ํ•„์š”ํ•˜๋‹ค.

 

GPG Key ์ƒ์„ฑ

โถ GPG ํŒจํ‚ค์ง€ ์„ค์น˜ (macOS ๊ธฐ์ค€)

brew install gpg

 

โท GPG ํ‚ค ์ƒ์„ฑ. ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ์˜ต์…˜ ์„ ํƒ ์งˆ๋ฌธ๋“ค์ด ๋‚˜์˜ค๋Š”๋ฐ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์„ค์ •ํ•ด๋„ ๋ฌด๋ฐฉํ•˜๋‹ค. Real name์€ GitHub ์‚ฌ์šฉ์ž ์ด๋ฆ„(username), Email address๋Š” GitHub์— ๋“ฑ๋กํ•œ ์ด๋ฉ”์ผ๊ณผ ๋™์ผํ•˜๊ฒŒ ์ž…๋ ฅ ํ•œ๋‹ค.

gpg --full-generate-key

# ===== ํ‚ค ์•”ํ˜ธํ™” ๋ฐฉ์‹ ์งˆ๋ฌธ =====
# Please select what kind of key you want: (9) ECC (sign and encrypt)
# Please select which elliptic curve you want: (1) Curve 25519
# Please specify how long the key should be valid: 0 = key does not expire

# ===== ์‚ฌ์šฉ์ž ์ •๋ณด ์งˆ๋ฌธ =====
# Real name: username
# Email address: your@email.com
# Comment: ์ƒ๋žต๊ฐ€๋Šฅ

 

๋ชจ๋“  ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•˜๋ฉด Passphrase(๋น„๋ฐ€๋ฒˆํ˜ธ)๋ฅผ ์ž…๋ ฅํ•˜๋Š” ํŒ์—…์ด ๋‚˜์˜จ๋‹ค. ์ƒ๋žตํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ๋ณด์•ˆ์„ ์œ„ํ•ด ์„ค์ •ํ•ด ๋‘๋Š” ๊ฑธ ์ถ”์ฒœํ•œ๋‹ค. ์„ค์ •ํ•œ Passphrase๋Š” ์„œ๋ช…์„ ํฌํ•จํ•œ ์ปค๋ฐ‹์„ ์ง„ํ–‰ํ•  ๋•Œ ํ•„์š”ํ•˜๋ฏ€๋กœ ์ž˜ ๊ธฐ์–ตํ•ด ๋‘”๋‹ค.

 

โธ GPG ํ‚ค๊ฐ€ ์ž˜ ์ƒ์„ฑ๋๋Š”์ง€ ํ™•์ธ (GPG ํ‚ค ๋ชฉ๋ก ์ถœ๋ ฅ)

gpg --list-keys

# pub   ed25519 2024-07-01 [SC]
#       GPG ID
# uid           [ultimate] ์ด๋ฆ„ <์ด๋ฉ”์ผ>
# sub   cv25519 2024-07-01 [E]

 

GPG ๊ณต๊ฐœํ‚ค ๋“ฑ๋ก

์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•ด์„œ GPG ๊ณต๊ฐœํ‚ค๋ฅผ ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌํ•œ๋‹ค.

# your@email.com ๋ถ€๋ถ„์€ ๋ณธ์ธ ์ด๋ฉ”์ผ ์ฃผ์†Œ๋กœ ๋ณ€๊ฒฝ
gpg --armor --export your@email.com | pbcopy

 

GitHub Settings - SSH and GPG keys ํŽ˜์ด์ง€์—์„œ New GPG key ๋ฒ„ํŠผ์„ ํด๋ฆญํ•œ๋‹ค. Title์€ ์‹๋ณ„ํ•˜๊ธฐ ์‰ฌ์šด ๊ฐ’์œผ๋กœ ์ž…๋ ฅํ•˜๊ณ , Key ํ•ญ๋ชฉ์—” ๋ณต์‚ฌํ•œ ๊ณต๊ฐœํ‚ค๋ฅผ ๋ถ™์—ฌ๋„ฃ๋Š”๋‹ค.

 

 

Secrets ๋“ฑ๋ก

์ด์ œ PG_PRIVATE_KEY, PASSPHRASE๋ฅผ ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์‹œํฌ๋ฆฟ์œผ๋กœ ๋“ฑ๋กํ•ด์•ผ ํ•œ๋‹ค. ํ„ฐ๋ฏธ๋„์— ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•ด์„œ GPG ๊ฐœ์ธํ‚ค๋ฅผ ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌํ•ด๋‘”๋‹ค.

# your@email.com ๋ถ€๋ถ„์€ ๋ณธ์ธ ์ด๋ฉ”์ผ ์ฃผ์†Œ๋กœ ๋ณ€๊ฒฝ
gpg --armor --export-secret-key your@email.com | pbcopy

 

GitHub ํ”„๋กœํ•„ ๋ ˆํฌ์ง€ํ† ๋ฆฌ - Settings - Secrets and variables - Actions๋กœ ์ด๋™ํ•œ ํ›„ PG_PRIVATE_KEY ์™€ PASSPHRASE ์ด๋ฆ„์˜ ์‹œํฌ๋ฆฟ์„ ์ถ”๊ฐ€ํ•œ๋‹ค. PG_PRIVATE_KEY ์‹œํฌ๋ฆฟ์—” ๋ฐฉ๊ธˆ ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌํ•œ ๋‚ด์šฉ์„ ๋ถ™์—ฌ๋„ฃ์œผ๋ฉด ๋˜๊ณ , PASSPHRASE ๋Š” GPG Key ์ƒ์„ฑ์‹œ ์„ค์ •ํ–ˆ๋˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ๋œ๋‹ค.

 

 

์œ„์ฒ˜๋Ÿผ ์‹œํฌ๋ฆฟ์„ ๋“ฑ๋กํ•ด๋‘๋ฉด ์›Œํฌํ”Œ๋กœ์šฐ ํŒŒ์ผ์—์„œ ${{ secrets.GPG_PRIVATE_KEY }} ๊ตฌ๋ฌธ์œผ๋กœ GPG ๊ฐœ์ธํ‚ค๋ฅผ ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

 

์›Œํฌํ”Œ๋กœ์šฐ ์ˆ˜์ •

ghaction-import-gpg ์•ก์…˜์„ ์‚ฌ์šฉํ•˜๋ฉด GPG Key๋ฅผ ๋” ์‰ฝ๊ฒŒ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ๋‹ค. YML ํŒŒ์ผ์„ ์•„๋ž˜์ฒ˜๋Ÿผ ์ˆ˜์ •ํ•˜์ž. your@email.com ๋ถ€๋ถ„์€ ๋ณธ์ธ ์ด๋ฉ”์ผ ์ฃผ์†Œ๋กœ ๋ณ€๊ฒฝํ•ด์ค€๋‹ค. git commit ๋ช…๋ น์–ด ๋ผ์ธ์„ ๋ณด๋ฉด -S ํ”Œ๋ž˜๊ทธ๊ฐ€ ์ถ”๊ฐ€๋๋Š”๋ฐ, ์ปค๋ฐ‹์„ ์ƒ์„ฑํ•  ๋•Œ GPG ํ‚ค๋กœ ์„œ๋ช…ํ•˜๋Š” ์˜ต์…˜์ด๋‹ค.

# rss-to-readme.yml

name: RSS to README

on:
  schedule:
    - cron: "0 */1 * * *" # ๋งค ์‹œ๊ฐ„ ์ •๊ฐ์— ์‹คํ–‰ (UTC)
  push:
    branches:
      - main # ์ง€์ •ํ•œ ๋ธŒ๋žœ์น˜์— push ํ•  ๋•Œ ์›Œํฌํ”Œ๋กœ์šฐ ์‹คํ–‰
  workflow_dispatch: # ์ˆ˜๋™ ํŠธ๋ฆฌ๊ฑฐ ๊ธฐ๋Šฅ ํ™œ์„ฑ

jobs:
  update-readme:
    runs-on: ubuntu-latest
    timeout-minutes: 30 # ์›Œํฌํ”Œ๋กœ์šฐ๊ฐ€ 30๋ถ„ ๋‚ด์— ์™„๋ฃŒ๋˜์ง€ ์•Š์œผ๋ฉด ์ž๋™ ์ข…๋ฃŒ

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Import GPG key
        uses: crazy-max/ghaction-import-gpg@v6
        with:
          gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
          passphrase: ${{ secrets.PASSPHRASE }}
          git_user_signingkey: true
          git_commit_gpgsign: true
          git_committer_name: GitHub Action Bot
          git_committer_email: your@email.com # ์ˆ˜์ •
          trust_level: 5 # Ultimate

      - name: Install Node.js
        uses: actions/setup-node@v4
        with:
          node-version: lts/*

      - uses: pnpm/action-setup@v4
        name: Install pnpm and dependencies
        with:
          version: latest
          run_install: true

      - name: Update README
        run: pnpm rss-to-readme # ์—…๋ฐ์ดํŠธ ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰

      - name: Commit and push if changed
        run: |
          # Git ์ €์žฅ์†Œ์— ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์žˆ์„ ๋•Œ๋งŒ ๋ช…๋ น์–ด ์‹คํ–‰
          if [ -n "$(git status --porcelain)" ]; then
            git add README.md
            git commit -S -m "Update README with latest articles [auto-generated]" -m "Generated at: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
            git push
          else
            echo "No changes to commit"
          fi

 

์›Œํฌํ”Œ๋กœ์šฐ ๋ณ€๊ฒฝ๋‚ด์šฉ์„ ํ”„๋กœํ•„ ๋ ˆํฌ์ง€ํ† ๋ฆฌ์— ๋ฐ˜์˜ํ•˜๊ณ  GitHub Actions๋ฅผ ํ†ตํ•ด ์ƒ์„ฑ๋œ ์ปค๋ฐ‹์„ ํ™•์ธํ•ด ๋ณด๋ฉด Verified ๋ฐฐ์ง€๊ฐ€ ์ถ”๊ฐ€๋œ๊ฑธ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 


๊ธ€ ์ˆ˜์ •์‚ฌํ•ญ์€ ๋…ธ์…˜ ํŽ˜์ด์ง€์— ๊ฐ€์žฅ ๋น ๋ฅด๊ฒŒ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”
๋ฐ˜์‘ํ˜•