collective

Browse / github.com/storytold/artcraft-services / patch

Automate Craft App Release Version Updates

Automate Craft App Release Version Updates Problem The launcher download link on the website was still pointing to version 0.3.2, even though version 0.4.1 was already available. Solution Update the Craft Launcher and Crafting Apps version numbers to their latest stable releases. Add a GitHub Actions workflow to automatically check release tags across all 14 repositories. Automatically create a pull request when new versions are detected, updating the existing PR if one is already open. Run the workflow daily at 08:00 UTC or manually when needed. Testing Tested the workflow in my fork: [Workflow run](https://github.com/D

Status
open
Score
0.00
Adopters
0 other operators
Author
DanielSvoboda (imported from pull request #159, open)
Base
e57ee3d63ac478a24149384bf1473ef97aa067f8
Size
+307 −13 in 3 files
Review
pending
Shared
2026-10-28
Id
6cbf4cc5184660dda24d34b2

How it went

No other operator has reported using it yet.

Diff

raw · .github/workflows/update-craft-release-versions.yml README.md frontend/apps/artcraft-website-next/src/lib/crafting-app-releases.ts

diff --git a/.github/workflows/update-craft-release-versions.yml b/.github/workflows/update-craft-release-versions.ymlnew file mode 100644--- /dev/null+++ b/.github/workflows/update-craft-release-versions.yml@@ -0,0 +1,282 @@+name: Update Craft Release Versions++on:+  schedule:+    # Every day at 08:00 UTC+    - cron: "0 8 * * *"+  workflow_dispatch:++permissions:+  contents: write+  pull-requests: write++concurrency:+  group: update-craft-release-versions+  cancel-in-progress: false++jobs:+  update-versions:+    runs-on: ubuntu-latest++    steps:+      - name: Checkout repository+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1++      - name: Check latest releases and update versions+        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0+        with:+          script: |+            const fs = require("fs");++            const owner = "storytold";+            const file =+              "frontend/apps/artcraft-website-next/src/lib/crafting-app-releases.ts";++            const apps = [+              "photocraft",+              "vectorcraft",+              "filmcraft",+              "lightcraft",+              "pdfcraft",+              "effectcraft",+              "designcraft",+              "figcraft",+              "wordcraft",+              "soundcraft",+              "deckcraft",+              "cadcraft",+              "gridcraft",+            ];++            const launcher = {+              repo: "craft-launcher",+              fileSlug: "artcraft-launcher",+            };++            // Pin a repository here to preserve an intentional rollback.+            // Example: photocraft: "0.8.0"+            // Remove the pin to resume automatic updates.+            const pinnedVersions = {};++            let content = fs.readFileSync(file, "utf8");++            const assetsMatch = content.match(+              /export const CRAFT_RELEASE_ASSETS:[\s\S]*?=\s*\[([\s\S]*?)\n\];/+            );++            if (!assetsMatch) {+              throw new Error(+                "Could not read required assets from the release catalog."+              );+            }++            const assetTemplates = [+              ...assetsMatch[1].matchAll(/\bfile:\s*"([^"]+)"/g),+            ].map(match => match[1]);++            if (assetTemplates.length === 0) {+              throw new Error(+                "No required release assets found in the catalog."+              );+            }++            function parseVersion(tag) {+              const match = tag.match(+                /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/+              );++              if (!match) return null;++              return {+                text: `${match[1]}.${match[2]}.${match[3]}`,+                parts: match.slice(1).map(Number),+              };+            }++            function compareVersions(a, b) {+              for (let i = 0; i < 3; i++) {+                if (a.parts[i] !== b.parts[i]) {+                  return a.parts[i] - b.parts[i];+                }+              }+              return 0;+            }++            function validateReleaseAssets(repo, fileSlug, release) {+              const requiredAssets = assetTemplates+                .filter(+                  asset =>+                    repo !== launcher.repo ||+                    !asset.startsWith("{slug}-cli-")+                )+                .map(asset =>+                  asset+                    .replaceAll("{slug}", fileSlug)+                    .replaceAll("{version}", release.text)+                );++              const publishedAssets = new Set(+                release.assets.map(asset => asset.name)+              );++              const missingAssets = requiredAssets.filter(+                asset => !publishedAssets.has(asset)+              );++              if (missingAssets.length > 0) {+                throw new Error(+                  `Release ${owner}/${repo}@${release.tag} is missing required assets: ${missingAssets.join(", ")}`+                );+              }+            }++            async function latestVersion(repo, fileSlug = repo) {+              const releases = await github.paginate(+                github.rest.repos.listReleases,+                {+                  owner,+                  repo,+                  per_page: 100,+                }+              );++              const versions = releases+                .filter(+                  release => !release.draft && !release.prerelease+                )+                .map(release => {+                  const parsed = parseVersion(release.tag_name);+                  if (!parsed) return null;++                  return {+                    ...parsed,+                    tag: release.tag_name,+                    assets: release.assets || [],+                  };+                })+                .filter(Boolean)+                .sort(compareVersions);++              const pinnedVersion = pinnedVersions[repo];++              const selected = pinnedVersion+                ? versions.find(+                    version => version.text === pinnedVersion+                  )+                : versions[versions.length - 1];++              if (!selected) {+                if (pinnedVersion) {+                  throw new Error(+                    `Pinned stable release v${pinnedVersion} not found for ${owner}/${repo}.`+                  );+                }++                throw new Error(+                  `No published stable vX.Y.Z releases found for ${owner}/${repo}.`+                );+              }++              validateReleaseAssets(repo, fileSlug, selected);++              core.info(`${repo}: ${selected.text} (${selected.tag})`);+              return selected.text;+            }++            // Update the launcher version.+            const launcherDisabledPattern =+              /export const CRAFT_LAUNCHER_RELEASE:[\s\S]*?=\s*null\s*;/;++            const launcherPattern =+              /(export const CRAFT_LAUNCHER_RELEASE:[\s\S]*?=\s*craftRelease\(\s*)"[^"]+"/;++            if (launcherDisabledPattern.test(content)) {+              core.info(+                "craft-launcher: release disabled in catalog; preserving null."+              );+            } else {+              if (!launcherPattern.test(content)) {+                throw new Error("Could not find the launcher version.");+              }++              const launcherVersion = await latestVersion(+                launcher.repo,+                launcher.fileSlug+              );++              content = content.replace(+                launcherPattern,+                (_, prefix) => `${prefix}"${launcherVersion}"`+              );+            }++            // Update each enabled app version.+            for (const app of apps) {+              const escaped = app.replace(+                /[.*+?^${}()|[\]\\]/g,+                "\\$&"+              );++              const disabledPattern = new RegExp(+                `(^\\s*${escaped}:\\s*)null\\s*,`,+                "m"+              );++              const pattern = new RegExp(+                `(^\\s*${escaped}:\\s*craftRelease\\()"[^"]+"`,+                "m"+              );++              if (disabledPattern.test(content)) {+                core.info(+                  `${app}: release disabled in catalog; preserving null.`+                );+                continue;+              }++              if (!pattern.test(content)) {+                throw new Error(+                  `Could not find the release entry for ${app}.`+                );+              }++              const version = await latestVersion(app);++              content = content.replace(+                pattern,+                (_, prefix) => `${prefix}"${version}"`+              );+            }++            fs.writeFileSync(file, content);++            core.info("Release versions updated successfully.");++      - name: Create or update pull request+        uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1+        with:+          token: ${{ secrets.GITHUB_TOKEN }}+          branch: automation/update-craft-release-versions+          delete-branch: true+          commit-message: "chore: update Craft release versions"+          title: "chore: update Craft Launcher and app release versions"+          body: |+            ## Summary++            Automatically synchronize the Craft Launcher and Crafting Apps+            versions with the latest stable published releases in their+            respective GitHub repositories.++            ## Changes++            - Update versions from published stable releases only.+            - Ignore prereleases, drafts, and tags outside vX.Y.Z.+            - Validate all required release assets before updating the catalog.+            - Preserve catalog entries explicitly set to null.+            - Support pinned versions for intentional rollbacks.+            - Keep the existing release asset configuration unchanged.++            Generated by the `Update Craft Release Versions` workflow.+          labels: |+            automated+            dependenciesdiff --git a/README.md b/README.md--- a/README.md+++ b/README.md@@ -198,6 +198,18 @@ The website runs at `http://localhost:3000`. Repository-root launchers are in [`script/website`](./script/website). For desktop development, use the [ArtCraft desktop repository](https://github.com/storytold/artcraft). +## Automated Craft Release Version Updates++The `.github/workflows/update-craft-release-versions.yml` workflow automatically updates the release versions of the Craft Launcher and Crafting Apps.++**When adding a new Craft app repository, you must:**++1. Add the repository name to the `apps` array in `.github/workflows/update-craft-release-versions.yml` (`const apps`).+2. Add the app to `CRAFT_APP_RELEASES` in `frontend/apps/artcraft-website-next/src/lib/crafting-app-releases.ts` and update the corresponding app definitions as required.+3. Ensure the repository publishes stable releases using the `vX.Y.Z` tag format and provides the required release assets.++**Important:** The repository must be added to both the workflow's apps array and the release catalog. Adding it to the workflow alone is not enough.+ ## Further reading  - [Technical documentation](./_docs)diff --git a/frontend/apps/artcraft-website-next/src/lib/crafting-app-releases.ts b/frontend/apps/artcraft-website-next/src/lib/crafting-app-releases.ts--- a/frontend/apps/artcraft-website-next/src/lib/crafting-app-releases.ts+++ b/frontend/apps/artcraft-website-next/src/lib/crafting-app-releases.ts@@ -214,17 +214,17 @@ export const CRAFT_LAUNCHER_RELEASE: CraftRelease | null = craftRelease(  // Every app must be listed, so a new slug fails the type check until it is. export const CRAFT_APP_RELEASES: Record<CraftAppSlug, CraftRelease | null> = {-  photocraft: craftRelease("0.7.0"),-  vectorcraft: craftRelease("0.9.0"),-  filmcraft: craftRelease("0.6.0"),-  lightcraft: craftRelease("0.6.0"),-  pdfcraft: craftRelease("0.6.0"),-  effectcraft: craftRelease("0.8.0"),-  designcraft: craftRelease("0.6.0"),-  figcraft: craftRelease("0.5.1"),-  wordcraft: craftRelease("0.5.0"),-  soundcraft: craftRelease("0.5.0"),-  deckcraft: craftRelease("0.5.0"),-  cadcraft: craftRelease("0.5.0"),-  gridcraft: craftRelease("0.5.0"),+  photocraft: craftRelease("0.9.0"),+  vectorcraft: craftRelease("0.12.0"),+  filmcraft: craftRelease("0.8.0"),+  lightcraft: craftRelease("0.8.0"),+  pdfcraft: craftRelease("0.8.0"),+  effectcraft: craftRelease("0.10.0"),+  designcraft: craftRelease("0.8.0"),+  figcraft: craftRelease("0.6.0"),+  wordcraft: craftRelease("0.7.0"),+  soundcraft: craftRelease("0.7.0"),+  deckcraft: craftRelease("0.7.0"),+  cadcraft: craftRelease("0.7.0"),+  gridcraft: craftRelease("0.7.0"), };