| 1870 | } |
| 1871 | |
| 1872 | export function updateWorkflows(args: CLIArgs) { |
| 1873 | const workflowsDir = WORKFLOWS_DIR; |
| 1874 | |
| 1875 | // 1. Update showcase_deploy.yml — add change detection + build job |
| 1876 | const deployPath = path.join(workflowsDir, "showcase_deploy.yml"); |
| 1877 | const deployState = probePath(deployPath); |
| 1878 | if (deployState === "unreadable") { |
| 1879 | console.error( |
| 1880 | `Error: ${deployPath} exists but is unreadable; refusing to silently skip workflow update.`, |
| 1881 | ); |
| 1882 | process.exit(1); |
| 1883 | } |
| 1884 | if (deployState === "missing") { |
| 1885 | console.warn( |
| 1886 | ` [WARN] ${deployPath} not found; skipping showcase_deploy.yml update.`, |
| 1887 | ); |
| 1888 | } |
| 1889 | if (deployState === "exists") { |
| 1890 | let deploy = fs.readFileSync(deployPath, "utf-8"); |
| 1891 | const slug = args.slug; |
| 1892 | const slugVar = slug.replace(/-/g, "_"); |
| 1893 | |
| 1894 | // Add to workflow_dispatch options if not present. If the regex doesn't |
| 1895 | // match the expected structure, assert loudly rather than silently |
| 1896 | // shipping a no-op change to the workflow file. |
| 1897 | // |
| 1898 | // The indent of the appended line is derived from the LAST existing |
| 1899 | // `- <entry>` line in the block (captured as `$2`) instead of being |
| 1900 | // hardcoded, so a YAML reflow that changes the block's indent doesn't |
| 1901 | // produce a misaligned insertion. |
| 1902 | // Anchor to a full option list line (leading whitespace, `- <slug>`, |
| 1903 | // trailing whitespace, end-of-line). A bare `includes("- <slug>")` |
| 1904 | // collides against longer sibling slugs (adding `foo` when `foo-bar` |
| 1905 | // exists already matches `"- foo-bar"`) and against stray |
| 1906 | // occurrences in comments/env vars/step names. |
| 1907 | const optionRe = new RegExp(`^\\s+- ${escapeRegex(slug)}\\s*$`, "m"); |
| 1908 | if (!optionRe.test(deploy)) { |
| 1909 | const before = deploy; |
| 1910 | deploy = deploy.replace( |
| 1911 | /(\s+options:\n(?:(\s+)- .+\n)+)/, |
| 1912 | `$1$2- ${slug}\n`, |
| 1913 | ); |
| 1914 | if (deploy === before) { |
| 1915 | throw new Error( |
| 1916 | `updateWorkflows: failed to locate the 'options:' block in ${deployPath}. ` + |
| 1917 | "The workflow file layout may have changed; update the regex in updateWorkflows().", |
| 1918 | ); |
| 1919 | } |
| 1920 | } |
| 1921 | |
| 1922 | // Anchor the outputs-block idempotency check to a line that starts |
| 1923 | // with `<slugVar>:` (optionally indented) — `includes("<slug>:")` |
| 1924 | // matches slug-shaped substrings inside env vars, step names, or |
| 1925 | // YAML comments and silently skips the insert. The guard MUST use |
| 1926 | // `slugVar` (underscore form) because the insertion below writes |
| 1927 | // `${slugVar}:` as the key; for hyphenated slugs (e.g. |
| 1928 | // `sales-dashboard` → `sales_dashboard`) matching on `slug` |
| 1929 | // (hyphen form) would never hit on a re-run and would duplicate |