diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2cba3c0a63..d3da631c81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,15 +46,26 @@ jobs: blog/**/*.{md,mdx} - name: Publish to Hashnode and Dev.to 📝 - if: github.ref == 'refs/heads/develop' && steps.changed-markdown-files.outputs.any_changed == 'true' env: HASHNODE_PAT: ${{ secrets.HASHNODE_PAT }} HASHNODE_PUBLICATION_ID: ${{ secrets.HASHNODE_PUBLICATION_ID }} - CHANGED_FILES: ${{ steps.changed-markdown-files.outputs.all_changed_files }} + ADDED_FILES: ${{ steps.changed-markdown-files.outputs.added_files }} DEVTO_API_KEY: ${{ secrets.DEVTO_API_KEY}} DEVTO_ORG_ID: ${{secrets.DEVTO_ORG_ID}} DEVTO_ORG_NAME: ${{secrets.DEVTO_ORG_NAME}} run: | cd ./publish-externals npm run generate - npx --yes tsx ./src/index.ts "$CHANGED_FILES" + if [ "$GITHUB_REF" == "refs/heads/develop" ]; then + npx --yes tsx ./src/index.ts --publish "$ADDED_FILES" + else + npx --yes tsx ./src/index.ts "$ADDED_FILES" + fi + + - name: Commit and push changes (on develop) + if: github.event_name == 'push' && github.ref == 'refs/heads/develop' + uses: stefanzweifel/git-auto-commit-action@v5 + with: + branch: develop + commit_author: Author + commit_message: "[ci skip] update snapshot" diff --git a/publish-externals/package.json b/publish-externals/package.json index 6fde7e5820..435c273be5 100644 --- a/publish-externals/package.json +++ b/publish-externals/package.json @@ -12,6 +12,7 @@ "dependencies": { "@apollo/client": "^3.10.8", "axios": "^1.7.2", + "crypto": "^1.0.1", "graphql": "^16.9.0", "gray-matter": "^4.0.3" }, diff --git a/publish-externals/snapshot.json b/publish-externals/snapshot.json new file mode 100644 index 0000000000..770fdd3117 --- /dev/null +++ b/publish-externals/snapshot.json @@ -0,0 +1,3 @@ +{ + "blogs": {} +} diff --git a/publish-externals/src/index.ts b/publish-externals/src/index.ts index 7d256fb582..b61f014f6a 100644 --- a/publish-externals/src/index.ts +++ b/publish-externals/src/index.ts @@ -2,35 +2,133 @@ import path from "path" import * as hashnode from "./utils/hashnode" import {extractFrontMatterAndContent} from "./utils/markdown" import * as devTo from "./utils/devto" +import snapshot from "../snapshot.json" +import fs from "fs" +import {createHash} from "crypto" -const ExternalPublications = [ +interface Blog { + file: string + platforms: { + [platform: string]: { + published: boolean + lastUpdatePublished: boolean + lastSuccessfulPublishedAt?: string + } + } + hash?: string +} + +interface ExternalPublication { + name: string + handler: (frontMatter: FrontMatter, content: string) => Promise +} + +interface FrontMatter { + slug: string +} + +const ExternalPublications: ExternalPublication[] = [ {name: "Hashnode", handler: hashnode.handler}, {name: "Dev.to", handler: devTo.handler}, ] const main = async () => { - const changedFiles = process.argv[2].split(" ") - const errors = [] - for (const file of changedFiles) { - if (file.startsWith("blog/")) { - const filePath = path.join(__dirname, "../../", file) - const {frontMatter, content} = extractFrontMatterAndContent(filePath) - for (let publication of ExternalPublications) { - console.log(`[${publication.name}] ${frontMatter.slug} ... publishing ⏳`) - try { - await publication.handler(frontMatter, content) - console.log(`[${publication.name}] Success ${frontMatter.slug} ... succeeded ✅`) - } catch (error) { - errors.push(error) - console.error(`[${publication.name}] Failure ${frontMatter.slug} ... failed 💀`) + const args = process.argv.slice(2) + const hasPublishFlag = args.includes("--publish") + const addedFilesArg = args.filter((arg) => arg !== "--publish")[0] + const addedFiles = addedFilesArg ? addedFilesArg.split(" ") : [] + const blogs: {[key: string]: Blog} = snapshot.blogs || {} + let toPublish = 0 + + try { + for (let publication of ExternalPublications) { + for (const file of addedFiles) { + const {frontMatter} = extractFrontMatterAndContent(path.join(__dirname, "../../", file)) + const slug = frontMatter.slug + if (file.startsWith("blog/")) { + console.log("Publishing new blog", slug) + toPublish++ + hasPublishFlag && (await publish(file, blogs, publication)) } } + + for (const slug in blogs) { + const file = blogs[slug].file + if (!addedFiles.includes(file)) { + const {content} = extractFrontMatterAndContent(path.join(__dirname, "../../", file)) + const contentHash = createHash("sha256").update(content).digest("hex") + + if (!blogs[slug].hash) { + console.log("Publishing new blog from snapshot", slug) + toPublish++ + hasPublishFlag && (await publish(file, blogs, publication)) + } else if (blogs[slug].hash !== contentHash) { + console.log("Publishing updated blog", slug) + toPublish++ + hasPublishFlag && (await publish(file, blogs, publication)) + } + } + } + } + } finally { + if (toPublish === 0) { + console.log("No changes detected. Exiting.") + } else { + hasPublishFlag && (await writeSnapshot(blogs)) } } +} + +const publish = async (file: string, blogs: {[key: string]: Blog}, publication: ExternalPublication) => { + const filePath = path.join(__dirname, "../../", file) + const {frontMatter, content} = extractFrontMatterAndContent(filePath) + const platform = publication.name + const contentHash = createHash("sha256").update(content).digest("hex") + const slug = frontMatter.slug + + console.log(`[${platform}] ${frontMatter.slug} ... publishing ⏳`) + try { + await publication.handler(frontMatter, content) + + blogs[slug] = blogs[slug] || { + file, + platforms: {}, + } + blogs[slug].platforms[platform] = { + published: true, + lastUpdatePublished: true, + lastSuccessfulPublishedAt: new Date().toUTCString(), + } + blogs[slug].hash = contentHash + + console.log(`[${platform}] Success ${frontMatter.slug} ... succeeded ✅`) + } catch (error) { + console.error(`[${platform}] Failure ${frontMatter.slug} ... failed 💀`) - if (errors.length !== 0) { - console.error(errors) - throw new Error("Publishing failed because of one or more errors") + blogs[slug] = blogs[slug] || { + file, + platforms: {}, + } + blogs[slug].platforms[platform] = { + published: blogs[slug].platforms[platform]?.published || false, + lastUpdatePublished: false, + } + console.log(blogs[slug]) + } +} + +const writeSnapshot = async (blogs: {[key: string]: Blog}) => { + console.log("Writing Snapshot") + const snapshot = JSON.stringify({blogs}, null, 2) + console.log(snapshot) + try { + fs.writeFile(path.join(__dirname, "../", "snapshot.json"), snapshot, (err) => { + if (err) { + console.error(err) + } + }) + } catch (err) { + console.error(err) } }