diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..f3777d7 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +# Catalog apps are Workday-maintained. Any PR touching catalog/ needs a +# review from the DevRel team. Adjust the team slug to the real one, and +# note this is enforced once branch protection requires code owner reviews. +/catalog/ @Workday/devrel diff --git a/.github/workflows/deploy-gallery.yml b/.github/workflows/deploy-gallery.yml index 74819aa..d9796b5 100644 --- a/.github/workflows/deploy-gallery.yml +++ b/.github/workflows/deploy-gallery.yml @@ -12,6 +12,7 @@ on: push: branches: [main] paths: + - "catalog/**" - "examples/**" - "site/**" - "hub.config.json" diff --git a/.github/workflows/validate-examples.yml b/.github/workflows/validate-examples.yml index d801590..18ab0ca 100644 --- a/.github/workflows/validate-examples.yml +++ b/.github/workflows/validate-examples.yml @@ -3,6 +3,7 @@ name: Validate examples on: pull_request: paths: + - "catalog/**" - "examples/**" - "scripts/**" - "hub.config.json" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f643376..8db845d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,11 @@ Thanks for helping build the open home for Workday Build examples. Adding an example is deliberately low effort: a folder, two small files, one validation command. +## Two sections + +- `examples/` is the community examples section. It is open to everyone, and it is where your PR goes. Everything in this guide is about it. +- `catalog/` holds Workday-built apps and is maintained by Workday (CODEOWNERS requires a DevRel review on any change there). Think something belongs in the catalog? Open an issue and we will take it from there. + ## Ways to contribute - **Add a new example.** The main event. See below. @@ -12,15 +17,25 @@ Thanks for helping build the open home for Workday Build examples. Adding an exa ## Add an example 1. **Fork** this repository and create a branch. -2. **Scaffold the folder.** From the repository root: +2. **Scaffold the folder.** From the repository root, pick whichever command runs on your machine; they produce identical folders: ```bash node scripts/new-example.mjs your-example-name --type "Extend App" ``` - This creates `examples/your-example-name/` with a prefilled `example.json` and README skeleton. (You can also copy `examples/_template/` by hand.) + No Node on your machine? Use the shell versions instead: + + ```bash + ./scripts/new-example.sh your-example-name --type "Extend App" + ``` -3. **Drop your artifact in.** Whatever it is: Extend app source exported from App Builder (via Local Disk Sync or the ZIP download), orchestration definitions, an agent skill as markdown, diagrams. The folder must be self-contained. + ```powershell + powershell -ExecutionPolicy Bypass -File scripts\new-example.ps1 your-example-name -Type "Extend App" + ``` + + Each creates `examples/your-example-name/` with a prefilled `example.json` and README skeleton. (You can also copy `examples/_template/` by hand.) + +3. **Drop your artifact in.** Whatever it is: Extend app source (exported with Local Disk Sync, the WDCLI, or the ZIP download), orchestration definitions, an agent skill as markdown, diagrams. The folder must be self-contained. 4. **Fill in the two files.** `example.json` needs a title, a description, and a type; everything else is optional. The README needs three short sections: What it is, What's inside, How to use it. 5. **Validate.** From the repository root: @@ -56,7 +71,7 @@ Useful references for what an example artifact looks like: - [Extend app components](https://developer.workday.com/doc/kwv1612374098305.md): what makes up an Extend app (amd, smd, pmd, business objects, and the rest). - [Local Disk Sync](https://developer.workday.com/doc/GUID-cbfd55e9-04f9-4480-879a-b63c42729a04-enHYPHENus.md): how to get your app source onto disk for submission. -- [App Builder](https://developer.workday.com/doc/zxh1651687589440.md): where Extend apps are built. +- [App Builder](https://developer.workday.com/doc/zxh1651687589440.md): one of the ways Extend apps get built. Your own IDE and the WDCLI work too; the hub does not care which tooling produced the artifact. - Official orchestration walkthroughs, for example [Create Workday Home Card Orchestration](https://developer.workday.com/doc/mwd1629844754304.md) and [Get and Create Workday Employee Data](https://developer.workday.com/doc/mxj1630014392721.md). ## Preview the gallery locally (optional) @@ -80,3 +95,5 @@ Workday DevRel reviews every pull request before merge. We look for: - **It is safe.** No secrets, no real data, nothing tenant-specific. We aim to respond within a few business days. Discussions on the PR are part of the process, so expect questions and suggestions rather than a silent merge or close. + +Merged examples are labeled in the gallery: **Workday** for examples authored by Workday teams, **Community** for everything else. Community examples are held to works, safe, and honest; Workday-authored ones get a stricter pass because people copy them as reference. diff --git a/README.md b/README.md index 8f45bd9..b121975 100644 --- a/README.md +++ b/README.md @@ -20,21 +20,23 @@ Browse working examples, copy them into your own projects, and contribute your o ## What is this? -Workday Build samples used to live in many places: the App Catalog, the docs, the forum. This repository brings them into one place that you can browse, copy from, and add to. +Workday Build examples used to live in many places: the App Catalog, the docs, the forum. This repository brings them into one place that you can browse, copy from, and add to. Every example: -- lives in its own folder under [`examples/`](examples) with everything it needs: Extend app source exported from App Builder, orchestration definitions, agent skills written as markdown, diagrams, whatever the artifact is. +- lives in its own folder under [`catalog/`](catalog) (Workday-built apps) or [`examples/`](examples) (community examples, open to everyone) with everything it needs: Extend app source, orchestration definitions, agent skills written as markdown, diagrams, whatever the artifact is. - ships with two small files: `example.json` (metadata that drives the index below and the optional gallery) and a README that says what it is and how to use it. - demonstrates a real Workday capability. Types, components, and products come from the approved lists in [`hub.config.json`](hub.config.json), and CI enforces them. ## Repository layout ``` -examples/ Every example is a self-contained folder: open it, read its README +catalog/ Workday-built apps, maintained by Workday +examples/ Community examples, open to external contributions _template/ Copy this (or run the scaffolder) to start a new example scripts/ new-example.mjs Scaffold a new example folder in one command + new-example.sh / .ps1 The same scaffolder for machines without Node validate-examples.mjs CI validation + README index generation site/ Optional Astro gallery (not required to use the examples) hub.config.json Repo URLs and the approved type, component, and product lists @@ -42,7 +44,7 @@ hub.config.json Repo URLs and the approved type, component, and product ## Example types -- 🧩 **Extend App**: full app source built in App Builder, ready to import into your development tenant. +- 🧩 **Extend App**: full app source, ready to deploy to your development tenant with whatever tooling you build with. - 🔌 **Integration App**: orchestration-driven integrations connecting Workday to other systems. - ⚙️ **Orchestration**: focused orchestration definitions for Orchestration Builder. - 🤖 **Agent Skill**: agent skills and instructions, written as markdown. @@ -52,34 +54,41 @@ hub.config.json Repo URLs and the approved type, component, and product ```bash git clone https://github.com/Workday/Developer-Relations -cd Developer-Relations/examples +cd Developer-Relations ``` -Open the folder you want and follow its README. What "use it" means depends on the type: +Open the folder you want under `catalog/` or `examples/` and follow its README. What "use it" means depends on the type: -- **Extend app source**: import the folder into App Builder against your WCP development tenant (or upload the ZIP to App Hub), then deploy, install, and launch. -- **Orchestrations and integration apps**: import into Orchestration Builder and point the credentials at your tenant. +- **Extend app source**: deploy to your WCP development tenant with your usual tooling (App Builder, the (VScode, Cursor, Claude code) plugins, or the WDCLI), then install and launch. +- **Orchestrations and integration apps**: import into Orchestration Builder, promote if your tenant needs it, and deploy to your tenant. - **Agent skills and reference material**: read, copy, adapt. -## All examples +## App catalog -This table is kept in sync with each example's `example.json` by `scripts/validate-examples.mjs`. +Workday-built apps, maintained by Workday. Both tables below are kept in sync with each entry's `example.json` by `scripts/validate-examples.mjs`. - + +| Example | Description | Type | +| --- | --- | --- | +| [`work-from-anywhere-extend-app`](catalog/work-from-anywhere-extend-app) | Enable employees to request to work from anywhere, for a manager to approve, and see requests on a calendar view. | Extend App | + + +## Examples -| Example | Description | Type | -| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------- | -| [`expense-policy-agent-skill`](examples/expense-policy-agent-skill) | A markdown skill that teaches an agent to answer expense policy questions and escalate anything it cannot verify. | Agent Skill | -| [`employee-data-orchestration`](examples/employee-data-orchestration) | An orchestration that reads worker data from one tenant and creates matching records through the Workday REST API. | Orchestration | -| [`work-from-anywhere-extend-app`](examples/work-from-anywhere-extend-app) | Enable employees to request to work from anywhere, for a manager to approve, and see requests on a calendar view. | Extend App | +Community examples, open to everyone. This is the section external contributions land in. + +| Example | Description | Type | +| --- | --- | --- | +| [`expense-policy-agent-skill`](examples/expense-policy-agent-skill) | A markdown skill that teaches an agent to answer expense policy questions and escalate anything it cannot verify. | Agent Skill | +| [`employee-data-orchestration`](examples/employee-data-orchestration) | An orchestration that reads worker data from one tenant and creates matching records through the Workday REST API. | Orchestration | ## Contributing -We want your examples, and adding one doesn't take much: +We want your examples. Community contributions go into `examples/` (the Examples section); the catalog is Workday-maintained, so if you think something belongs there, open an issue instead. Adding an example doesn't take much: -1. Scaffold a folder: `node scripts/new-example.mjs your-example-name` +1. Scaffold a folder: `node scripts/new-example.mjs your-example-name`. No Node? `./scripts/new-example.sh` (macOS, Linux) and `scripts\new-example.ps1` (Windows) do the same thing. 2. Drop your artifact in, and fill in the generated `example.json` and README. 3. Validate: `node scripts/validate-examples.mjs` 4. Open a pull request. Workday DevRel reviews every submission before merge. @@ -110,6 +119,10 @@ There is no hosted version while this repository is private. Once it is public, - **Bugs in an example**: open an [issue](https://github.com/Workday/Developer-Relations/issues) using the bug report template. - **New example proposals**: open an issue with the proposal template before you build, if you want early feedback. +## Use at your own pace, verify everything + +Everything in this repository is provided as is, without warranty of any kind. Examples are starting points, not production software. Review the code, adapt it to your configuration, and always test in a non-production tenant before deploying anything to a tenant you care about. Submissions are reviewed before merge, but review does not replace your own verification. + ## License License terms are being finalized. Until a LICENSE file lands in this repository, treat the examples as reference material for building on Workday. diff --git a/catalog/README.md b/catalog/README.md new file mode 100644 index 0000000..80cb849 --- /dev/null +++ b/catalog/README.md @@ -0,0 +1,7 @@ +# App catalog + +Workday-built apps, maintained by Workday. Every folder here is a complete app: open it, read its README, and deploy it to your development tenant with whatever tooling you build with. + +This section is not open to external pull requests; CODEOWNERS requires a DevRel review on any change. Think something belongs here? Open an issue and we will take it from there. + +Contributing your own work? That lives in [examples/](../examples/), and the [contributing guide](../CONTRIBUTING.md) covers the whole flow. diff --git a/examples/work-from-anywhere-extend-app/README.md b/catalog/work-from-anywhere-extend-app/README.md similarity index 56% rename from examples/work-from-anywhere-extend-app/README.md rename to catalog/work-from-anywhere-extend-app/README.md index 5196b12..820f6b1 100644 --- a/examples/work-from-anywhere-extend-app/README.md +++ b/catalog/work-from-anywhere-extend-app/README.md @@ -4,26 +4,20 @@ An Extend app that lets employees request to work from anywhere, routes the request to their manager for approval, and shows everyone's arrangements on a calendar view. -Status: this is a sample entry that demonstrates the hub's format. The `app-source/` folder holds a placeholder until the real App Builder export lands. +Status: this is a sample entry that demonstrates the hub's format. The `app-source/` folder holds a placeholder until the real app source export lands. ## What's inside -- `app-source/` is where the app source exported from App Builder lives: app metadata (amd), site metadata (smd), pages (pmd), business objects, and security domains. +- `app-source/` is where the exported app source lives: app metadata (amd), site metadata (smd), pages (pmd), business objects, and security domains. - `example.json` drives the card in the gallery and the index table in the repository README. ## Deploy instructions -### App Builder (recommended) +Use whatever tooling you normally build with: -1. Open App Builder against your WCP development tenant. -2. Import this folder with Open a Local Folder, or upload the source as a ZIP to App Hub. -3. Save and Deploy, then install and launch the app in your tenant. - -### IntelliJ plugin (alternative) - -1. Install the Workday Extend Plugin for IntelliJ. -2. Open this folder in IntelliJ. -3. Log in to Workday Extend and your tenant, then deploy the app to the tenant. +- **App Builder**: import this folder (or upload the source as a ZIP to App Hub), Save and Deploy, then install and launch in your tenant. +- **IntelliJ plugin**: open the folder, log in to Workday Extend and your tenant, then deploy to the tenant. +- **WDCLI**: push the app source to App Hub from your terminal as the developer CLI rolls out. ## Configuration diff --git a/catalog/work-from-anywhere-extend-app/app-source/PLACEHOLDER.md b/catalog/work-from-anywhere-extend-app/app-source/PLACEHOLDER.md new file mode 100644 index 0000000..fd93d13 --- /dev/null +++ b/catalog/work-from-anywhere-extend-app/app-source/PLACEHOLDER.md @@ -0,0 +1,3 @@ +# App source goes here + +Replace this folder's contents with your exported app source (Local Disk Sync, the WDCLI, or the ZIP download from App Hub). The export includes the amd and smd metadata, pmd pages, business objects, and security domains. diff --git a/catalog/work-from-anywhere-extend-app/example.json b/catalog/work-from-anywhere-extend-app/example.json new file mode 100644 index 0000000..fdbec79 --- /dev/null +++ b/catalog/work-from-anywhere-extend-app/example.json @@ -0,0 +1,19 @@ +{ + "title": "Work From Almost Anywhere", + "description": "Enable employees to request to work from anywhere, for a manager to approve, and see requests on a calendar view.", + "type": "Extend App", + "components": [ + "Presentation", + "Model", + "Template" + ], + "products": [ + "Workday Extend", + "Workday HCM" + ], + "authors": [ + "obinnacodes" + ], + "tutorial": "", + "source": "workday" +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..86b8359 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,11 @@ +# Examples + +The community section, open to everyone. Every folder here is a self-contained example: the artifact plus an `example.json` and a README that says what it is and how to use it. + +Add your own in minutes: + +- `node scripts/new-example.mjs my-example-name` from the repository root +- No Node? `./scripts/new-example.sh` (macOS, Linux) or `scripts\new-example.ps1` (Windows) +- No tooling at all? Copy [_template/](_template/) by hand, even from the GitHub web UI + +Fill in the two generated files, drop your artifact in, and open a pull request. The [contributing guide](../CONTRIBUTING.md) has the details, and Workday DevRel reviews every submission before merge. diff --git a/examples/_template/README.md b/examples/_template/README.md index 2df33c1..0cec834 100644 --- a/examples/_template/README.md +++ b/examples/_template/README.md @@ -8,7 +8,7 @@ One short paragraph: what this example shows and who it is for. If it maps to a Bullet the contents of this folder so a reader knows what they are looking at. For instance: -- `app/` holds the Extend app source exported from App Builder +- `app/` holds the exported Extend app source - `orchestrations/` holds the orchestration definitions - `SKILL.md` is the agent skill - `diagrams/` holds the architecture drawings @@ -17,8 +17,8 @@ Bullet the contents of this folder so a reader knows what they are looking at. F The concrete steps to put this example to work, whatever that means for this artifact. Examples: -- **Extend app source**: import the folder into App Builder against your WCP development tenant (or upload the ZIP to App Hub), then deploy, install, and launch. -- **Orchestration**: import it in Orchestration Builder and point the credentials at your tenant. +- **Extend app source**: deploy to your WCP development tenant with your usual tooling (App Builder, the IDE plugins, or the WDCLI), then install and launch. +- **Orchestration**: import it in Orchestration Builder, promote if your tenant needs it, and deploy to your tenant. - **Agent skill or reference material**: read it, copy it, adapt it. If your example needs configuration (credentials, tenant URLs), document the variables here and never commit real values. @@ -38,5 +38,6 @@ Metadata is JSON only. All fields: | `products` | No | Workday products touched, from the `products` list in `hub.config.json` | | `authors` | No | GitHub usernames | | `tutorial` | No | An https link to a walkthrough. Adds a Tutorial link to the card. Omit or leave empty if none | +| `source` | No | Leave it as `community` (the default). DevRel sets `workday` on examples authored by Workday teams, which controls the badge in the gallery | Validate from the repository root with `node scripts/validate-examples.mjs`. diff --git a/examples/_template/example.json b/examples/_template/example.json index 8c6d435..58751ca 100644 --- a/examples/_template/example.json +++ b/examples/_template/example.json @@ -5,5 +5,6 @@ "components": [], "products": [], "authors": [], - "tutorial": "" + "tutorial": "", + "source": "community" } diff --git a/examples/employee-data-orchestration/README.md b/examples/employee-data-orchestration/README.md index ff8ee95..fff6447 100644 --- a/examples/employee-data-orchestration/README.md +++ b/examples/employee-data-orchestration/README.md @@ -13,10 +13,10 @@ Status: this is a sample entry that demonstrates the hub's format. The `orchestr ## How to use it -1. Open Orchestration Builder in your development tenant. -2. Import the orchestration definition from `orchestration/`. -3. Point the orchestration credentials at your tenant's Integration System User. -4. Run it, then check Orchestration Activity for the execution log. +1. Import the orchestration definition from `orchestration/` in Orchestration Builder. +2. Point the orchestration credentials at your tenant's Integration System User. +3. With Workday Extend, push straight to your development tenant and run it. Without Extend, promote the orchestration to IMPL, then deploy it to your tenant. +4. Check Orchestration Activity for the execution log. ## Related documentation diff --git a/examples/employee-data-orchestration/example.json b/examples/employee-data-orchestration/example.json index 3bfeaeb..ecd1e0d 100644 --- a/examples/employee-data-orchestration/example.json +++ b/examples/employee-data-orchestration/example.json @@ -2,8 +2,16 @@ "title": "Get and Create Employee Data", "description": "An orchestration that reads worker data from one tenant and creates matching records through the Workday REST API.", "type": "Orchestration", - "components": ["Orchestration"], - "products": ["Workday Orchestrate", "Workday REST API"], - "authors": ["obinnacodes"], - "tutorial": "https://developer.workday.com/doc/mxj1630014392721.md" + "components": [ + "Orchestration" + ], + "products": [ + "Workday Orchestrate", + "Workday REST API" + ], + "authors": [ + "obinnacodes" + ], + "tutorial": "https://developer.workday.com/doc/mxj1630014392721.md", + "source": "workday" } diff --git a/examples/expense-policy-agent-skill/example.json b/examples/expense-policy-agent-skill/example.json index 7fb874d..09aaefc 100644 --- a/examples/expense-policy-agent-skill/example.json +++ b/examples/expense-policy-agent-skill/example.json @@ -5,5 +5,6 @@ "components": [], "products": ["Workday Financials"], "authors": ["obinnacodes"], - "tutorial": "" + "tutorial": "", + "source": "community" } diff --git a/examples/work-from-anywhere-extend-app/app-source/PLACEHOLDER.md b/examples/work-from-anywhere-extend-app/app-source/PLACEHOLDER.md deleted file mode 100644 index 60654e1..0000000 --- a/examples/work-from-anywhere-extend-app/app-source/PLACEHOLDER.md +++ /dev/null @@ -1,3 +0,0 @@ -# App source goes here - -Replace this folder's contents with the app source exported from App Builder, either through Local Disk Sync or the ZIP download from App Hub. The export includes the amd and smd metadata, pmd pages, business objects, and security domains. diff --git a/examples/work-from-anywhere-extend-app/example.json b/examples/work-from-anywhere-extend-app/example.json deleted file mode 100644 index 6f2630e..0000000 --- a/examples/work-from-anywhere-extend-app/example.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "title": "Work From Almost Anywhere", - "description": "Enable employees to request to work from anywhere, for a manager to approve, and see requests on a calendar view.", - "type": "Extend App", - "components": ["Presentation", "Model", "Template"], - "products": ["Workday Extend", "Workday HCM"], - "authors": ["obinnacodes"], - "tutorial": "" -} diff --git a/scripts/new-example.mjs b/scripts/new-example.mjs index 9926c9c..82506f2 100644 --- a/scripts/new-example.mjs +++ b/scripts/new-example.mjs @@ -1,10 +1,13 @@ #!/usr/bin/env node -// Creates a new example folder with the two files every example needs. +// Creates a new example folder from examples/_template. // // node scripts/new-example.mjs my-example-name // node scripts/new-example.mjs my-example-name --type "Orchestration" --title "My Example" +// +// No Node on your machine? scripts/new-example.sh (macOS, Linux) and +// scripts/new-example.ps1 (Windows) do the same thing. -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { cpSync, existsSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -33,36 +36,18 @@ if (existsSync(dir)) { bail(`examples/${name} already exists. Pick another name.`); } -mkdirSync(dir, { recursive: true }); - -const meta = { - title, - description: "One or two sentences about what this example shows.", - type, - components: [], - products: [], - authors: [], - tutorial: "" -}; -writeFileSync(join(dir, "example.json"), JSON.stringify(meta, null, 2) + "\n"); - -writeFileSync( - join(dir, "README.md"), - `# ${title} - -## What it is - -One short paragraph: what this example shows and who it is for. - -## What's inside - -- Bullet the contents of this folder so a reader knows what they are looking at. +cpSync(join(repoRoot, "examples", "_template"), dir, { recursive: true }); -## How to use it +const metaPath = join(dir, "example.json"); +const meta = JSON.parse(readFileSync(metaPath, "utf8")); +meta.title = title; +meta.type = type; +writeFileSync(metaPath, JSON.stringify(meta, null, 2) + "\n"); -The concrete steps to put this example to work: import into App Builder and deploy to your development tenant, import the orchestration, read and adapt the skill, or whatever fits this artifact. -` -); +const readmePath = join(dir, "README.md"); +const readmeLines = readFileSync(readmePath, "utf8").split("\n"); +readmeLines[0] = `# ${title}`; +writeFileSync(readmePath, readmeLines.join("\n")); console.log(`Created examples/${name} (type: ${type})`); console.log(""); diff --git a/scripts/new-example.ps1 b/scripts/new-example.ps1 new file mode 100644 index 0000000..24c8e05 --- /dev/null +++ b/scripts/new-example.ps1 @@ -0,0 +1,60 @@ +# Same as new-example.mjs but for Windows machines without Node. Copies +# examples/_template and fills in the title and type. +# +# powershell -ExecutionPolicy Bypass -File scripts\new-example.ps1 my-example-name +# powershell -ExecutionPolicy Bypass -File scripts\new-example.ps1 my-example-name -Type "Orchestration" -Title "My Example" + +param( + [Parameter(Position=0)][string]$Name, + [string]$Type, + [string]$Title +) + +$ErrorActionPreference = "Stop" + +function Fail($msg) { + Write-Host $msg + exit 1 +} + +$root = Split-Path -Parent $PSScriptRoot +$config = Get-Content "$root\hub.config.json" -Raw | ConvertFrom-Json + +if (-not $Name -or $Name.StartsWith("-")) { + Fail 'Usage: powershell -ExecutionPolicy Bypass -File scripts\new-example.ps1 [-Type "Extend App"] [-Title "My Example"]' +} +if ($Name -cnotmatch '^[a-z0-9][a-z0-9-]*$') { + Fail "Folder names are kebab-case: lowercase letters, numbers, and hyphens. `"$Name`" is not." +} + +if (-not $Type) { $Type = $config.types[0] } +if ($config.types -notcontains $Type) { + Fail "`"$Type`" is not an approved type. Pick from: $($config.types -join ', ')" +} + +if (-not $Title) { + $Title = ($Name -split '-' | ForEach-Object { $_.Substring(0,1).ToUpper() + $_.Substring(1) }) -join ' ' +} + +$dir = "$root\examples\$Name" +if (Test-Path $dir) { + Fail "examples/$Name already exists. Pick another name." +} + +Copy-Item "$root\examples\_template" $dir -Recurse + +$meta = Get-Content "$dir\example.json" -Raw | ConvertFrom-Json +$meta.title = $Title +$meta.type = $Type +$meta | ConvertTo-Json -Depth 5 | Set-Content "$dir\example.json" -Encoding UTF8 + +$readme = @(Get-Content "$dir\README.md") +$readme[0] = "# $Title" +$readme | Set-Content "$dir\README.md" -Encoding UTF8 + +Write-Host "Created examples/$Name (type: $Type)" +Write-Host "" +Write-Host "Next steps:" +Write-Host " 1. Drop your artifact into examples/$Name/ (app source, orchestration, skill markdown, diagrams)." +Write-Host " 2. Edit examples/$Name/example.json and README.md." +Write-Host " 3. Open a pull request. CI runs validation for you." diff --git a/scripts/new-example.sh b/scripts/new-example.sh new file mode 100755 index 0000000..ec0e673 --- /dev/null +++ b/scripts/new-example.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Same as new-example.mjs but with no Node needed. Copies examples/_template +# and fills in the title and type. +# +# ./scripts/new-example.sh my-example-name +# ./scripts/new-example.sh my-example-name --type "Orchestration" --title "My Example" + +set -euo pipefail + +root="$(cd "$(dirname "$0")/.." && pwd)" + +name="${1:-}" +if [ -z "$name" ] || [[ "$name" == --* ]]; then + echo 'Usage: ./scripts/new-example.sh [--type "Extend App"] [--title "My Example"]' >&2 + exit 1 +fi +shift + +type="" +title="" +while [ $# -gt 0 ]; do + case "$1" in + --type) type="${2:-}"; shift 2 ;; + --title) title="${2:-}"; shift 2 ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +if ! echo "$name" | grep -Eq '^[a-z0-9][a-z0-9-]*$'; then + echo "Folder names are kebab-case: lowercase letters, numbers, and hyphens. \"$name\" is not." >&2 + exit 1 +fi + +# hub.config.json is the source of truth for types. Most machines don't have +# jq, so grab the types block with awk and grep inside it. +types=$(awk '/"types": \[/,/\]/' "$root/hub.config.json") + +if [ -z "$type" ]; then + type=$(echo "$types" | grep -o '"[^"]*"' | sed -n 2p | tr -d '"') +fi + +if ! echo "$types" | grep -qF "\"$type\""; then + echo "\"$type\" is not an approved type. Check the types list in hub.config.json." >&2 + exit 1 +fi + +if [ -z "$title" ]; then + # my-example-name -> My Example Name + title=$(echo "$name" | tr '-' ' ' | awk '{for (i=1; i<=NF; i++) $i = toupper(substr($i,1,1)) substr($i,2)} 1') +elif ! echo "$title" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9 .,()-]*$'; then + echo "Keep the title to letters, numbers, spaces, and basic punctuation." >&2 + exit 1 +fi + +dir="$root/examples/$name" +if [ -e "$dir" ]; then + echo "examples/$name already exists. Pick another name." >&2 + exit 1 +fi + +cp -R "$root/examples/_template" "$dir" + +# BSD and GNU sed disagree about -i, so go through a temp file +sed "s/\"title\": \"My Example\"/\"title\": \"$title\"/; s/\"type\": \"Extend App\"/\"type\": \"$type\"/" "$dir/example.json" > "$dir/.tmp" +mv "$dir/.tmp" "$dir/example.json" + +sed "1s/.*/# $title/" "$dir/README.md" > "$dir/.tmp" +mv "$dir/.tmp" "$dir/README.md" + +echo "Created examples/$name (type: $type)" +echo +echo "Next steps:" +echo " 1. Drop your artifact into examples/$name/ (app source, orchestration, skill markdown, diagrams)." +echo " 2. Edit examples/$name/example.json and README.md." +echo " 3. Open a pull request. CI runs validation for you." diff --git a/scripts/validate-examples.mjs b/scripts/validate-examples.mjs index 31ce9f8..19152f5 100644 --- a/scripts/validate-examples.mjs +++ b/scripts/validate-examples.mjs @@ -1,79 +1,98 @@ #!/usr/bin/env node -// Checks every folder in examples/ and keeps the "All examples" table -// in the root README up to date. +// Checks every entry in catalog/ and examples/ and keeps the two tables +// in the root README up to date. Zero dependencies. // -// node scripts/validate-examples.mjs validate and refresh the README table -// node scripts/validate-examples.mjs --check validate only, fail if the table is stale (CI) +// node scripts/validate-examples.mjs validate and refresh the README tables +// node scripts/validate-examples.mjs --check validate only, fail if a table is stale (CI) import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const repoRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); -const examplesDir = join(repoRoot, "examples"); const readmePath = join(repoRoot, "README.md"); const checkOnly = process.argv.includes("--check"); const config = JSON.parse(readFileSync(join(repoRoot, "hub.config.json"), "utf8")); +// catalog/ holds Workday-built apps, examples/ holds community examples. +const sections = [ + { dir: "catalog", markers: "catalog", defaultSource: "workday" }, + { dir: "examples", markers: "examples", defaultSource: "community" } +]; + const errors = []; -const examples = []; +const entries = []; -for (const name of readdirSync(examplesDir).sort()) { - // _template and dotfiles are not examples - if (name.startsWith("_") || name.startsWith(".")) continue; - const dir = join(examplesDir, name); - if (!statSync(dir).isDirectory()) continue; +for (const section of sections) { + const sectionDir = join(repoRoot, section.dir); + if (!existsSync(sectionDir)) continue; - if (!existsSync(join(dir, "README.md"))) { - errors.push(`${name}: missing README.md`); - } + for (const name of readdirSync(sectionDir).sort()) { + // _template and dotfiles are not entries + if (name.startsWith("_") || name.startsWith(".")) continue; + const dir = join(sectionDir, name); + if (!statSync(dir).isDirectory()) continue; - const metaPath = join(dir, "example.json"); - if (!existsSync(metaPath)) { - errors.push(`${name}: missing example.json`); - continue; - } + if (!existsSync(join(dir, "README.md"))) { + errors.push(`${section.dir}/${name}: missing README.md`); + } - let meta; - try { - meta = JSON.parse(readFileSync(metaPath, "utf8")); - } catch (err) { - errors.push(`${name}/example.json is not valid JSON: ${err.message}`); - continue; - } + const metaPath = join(dir, "example.json"); + if (!existsSync(metaPath)) { + errors.push(`${section.dir}/${name}: missing example.json`); + continue; + } - if (!meta.title) errors.push(`${name}: example.json needs a "title"`); - if (!meta.description) errors.push(`${name}: example.json needs a "description"`); + let meta; + try { + meta = JSON.parse(readFileSync(metaPath, "utf8")); + } catch (err) { + errors.push(`${section.dir}/${name}/example.json is not valid JSON: ${err.message}`); + continue; + } - if (!meta.type) { - errors.push(`${name}: example.json needs a "type"`); - } else if (!config.types.includes(meta.type)) { - errors.push(`${name}: "${meta.type}" is not an approved type. Pick from: ${config.types.join(", ")}`); - } + if (!meta.title) errors.push(`${section.dir}/${name}: example.json needs a "title"`); + if (!meta.description) errors.push(`${section.dir}/${name}: example.json needs a "description"`); - for (const component of asList(meta.components)) { - if (!config.components.includes(component)) { - errors.push(`${name}: "${component}" is not an approved component. Pick from: ${config.components.join(", ")}`); + if (!meta.type) { + errors.push(`${section.dir}/${name}: example.json needs a "type"`); + } else if (!config.types.includes(meta.type)) { + errors.push(`${section.dir}/${name}: "${meta.type}" is not an approved type. Pick from: ${config.types.join(", ")}`); } - } - for (const product of asList(meta.products)) { - if (!config.products.includes(product)) { - errors.push(`${name}: "${product}" is not an approved product. Pick from: ${config.products.join(", ")}`); + for (const component of asList(meta.components)) { + if (!config.components.includes(component)) { + errors.push(`${section.dir}/${name}: "${component}" is not an approved component. Pick from: ${config.components.join(", ")}`); + } } - } - if (meta.tutorial && !meta.tutorial.startsWith("https://")) { - errors.push(`${name}: "tutorial" should be an https link, or left out`); - } + for (const product of asList(meta.products)) { + if (!config.products.includes(product)) { + errors.push(`${section.dir}/${name}: "${product}" is not an approved product. Pick from: ${config.products.join(", ")}`); + } + } + + if (meta.tutorial && !meta.tutorial.startsWith("https://")) { + errors.push(`${section.dir}/${name}: "tutorial" should be an https link, or left out`); + } + + if (meta.source && !["workday", "community"].includes(meta.source)) { + errors.push(`${section.dir}/${name}: "source" must be "workday" or "community", or left out`); + } + if (section.dir === "catalog" && meta.source === "community") { + errors.push(`catalog/${name}: catalog apps are Workday-maintained, so "source" cannot be "community". Community submissions live in examples/.`); + } - examples.push({ - id: name, - title: meta.title || name, - description: meta.description || "", - type: meta.type || "" - }); + entries.push({ + id: name, + sectionMarkers: section.markers, + path: `${section.dir}/${name}`, + title: meta.title || name, + description: meta.description || "", + type: meta.type || "" + }); + } } if (errors.length > 0) { @@ -83,29 +102,34 @@ if (errors.length > 0) { process.exit(1); } -examples.sort((a, b) => a.title.localeCompare(b.title)); +entries.sort((a, b) => a.title.localeCompare(b.title)); -// Compare the table by content, not formatting, so tools like Prettier -// can reflow it without the check calling it stale. const readme = readFileSync(readmePath, "utf8"); -const expectedRows = examples.map((example) => [ - `[\`${example.id}\`](examples/${example.id})`, - example.description, - example.type -]); -const inSync = JSON.stringify(tableRows(readme)) === JSON.stringify(expectedRows); + +// Compare each table by content, not formatting, so tools like Prettier +// can reflow them without the check calling them stale. +let allInSync = true; +for (const section of sections) { + const expected = rowsFor(section.markers); + const current = tableRows(readme, section.markers); + if (JSON.stringify(current) !== JSON.stringify(expected)) allInSync = false; +} if (checkOnly) { - if (!inSync) { - console.error("README.md example table is out of date. Run: node scripts/validate-examples.mjs"); + if (!allInSync) { + console.error("A README table is out of date. Run: node scripts/validate-examples.mjs"); process.exit(1); } - console.log(`OK: ${examples.length} example(s) validated, README table in sync.`); -} else if (inSync) { - console.log(`Validated ${examples.length} example(s), README table already up to date.`); + console.log(`OK: ${entries.length} entr${entries.length === 1 ? "y" : "ies"} validated, README tables in sync.`); +} else if (allInSync) { + console.log(`Validated ${entries.length} entr${entries.length === 1 ? "y" : "ies"}, README tables already up to date.`); } else { - writeFileSync(readmePath, withFreshTable(readme)); - console.log(`Validated ${examples.length} example(s) and updated README.md.`); + let updated = readme; + for (const section of sections) { + updated = withFreshTable(updated, section.markers); + } + writeFileSync(readmePath, updated); + console.log(`Validated ${entries.length} entr${entries.length === 1 ? "y" : "ies"} and updated README.md.`); } function asList(value) { @@ -114,8 +138,26 @@ function asList(value) { return []; } -function tableRows(text) { - const [startAt, endAt] = markerPositions(text); +function rowsFor(markers) { + return entries + .filter((entry) => entry.sectionMarkers === markers) + .map((entry) => [`[\`${entry.id}\`](${entry.path})`, entry.description, entry.type]); +} + +function markerPositions(text, markers) { + const start = ``; + const end = ``; + const startAt = text.indexOf(start); + const endAt = text.indexOf(end); + if (startAt === -1 || endAt === -1) { + console.error(`README.md is missing the ${start} / ${end} markers.`); + process.exit(1); + } + return [startAt + start.length, endAt]; +} + +function tableRows(text, markers) { + const [startAt, endAt] = markerPositions(text, markers); const rows = []; for (const line of text.slice(startAt, endAt).split("\n")) { const trimmed = line.trim(); @@ -128,23 +170,9 @@ function tableRows(text) { return rows; } -function markerPositions(text) { - const start = ""; - const end = ""; - const startAt = text.indexOf(start); - const endAt = text.indexOf(end); - if (startAt === -1 || endAt === -1) { - console.error(`README.md is missing the ${start} / ${end} markers.`); - process.exit(1); - } - return [startAt + start.length, endAt]; -} - -function withFreshTable(text) { - const [startAt, endAt] = markerPositions(text); - const rows = examples.map( - (example) => `| [\`${example.id}\`](examples/${example.id}) | ${example.description} | ${example.type} |` - ); +function withFreshTable(text, markers) { + const [startAt, endAt] = markerPositions(text, markers); + const rows = rowsFor(markers).map((cells) => `| ${cells.join(" | ")} |`); const table = ["| Example | Description | Type |", "| --- | --- | --- |", ...rows].join("\n"); return text.slice(0, startAt) + "\n" + table + "\n" + text.slice(endAt); } diff --git a/site/src/components/Card.astro b/site/src/components/Card.astro index aba44a3..c57c896 100644 --- a/site/src/components/Card.astro +++ b/site/src/components/Card.astro @@ -1,38 +1,46 @@ --- import TypeIcon from "./TypeIcon.astro"; -import { withBase } from "../lib/examples.js"; +import { withBase, sourceLabel } from "../lib/examples.js"; const { example } = Astro.props; -const detailUrl = withBase(`examples/${example.id}/`); +const detailUrl = withBase(`${example.path}/`); +const badge = sourceLabel(example); const tags = [...new Set([example.type, ...example.components, ...example.products])]; -const searchText = [example.title, example.description, example.id, example.type, ...example.components, ...example.products] +const searchText = [example.title, example.description, example.id, example.type, badge, ...example.components, ...example.products] .join(" ") .toLowerCase(); ---
- + {badge === "Workday" ? ( + + + Workday + + ) : ( + + + Community + + )}

{example.title}

{example.description}

diff --git a/site/src/components/EntryPage.astro b/site/src/components/EntryPage.astro new file mode 100644 index 0000000..a0fd274 --- /dev/null +++ b/site/src/components/EntryPage.astro @@ -0,0 +1,144 @@ +--- +import "../styles/global.css"; +import Header from "./Header.astro"; +import Footer from "./Footer.astro"; +import { config, withBase, sourceLabel } from "../lib/examples.js"; + +const { example } = Astro.props; + +const readmes = { + ...import.meta.glob("../../../catalog/*/README.md", { eager: true }), + ...import.meta.glob("../../../examples/*/README.md", { eager: true }) +}; +const readme = Object.entries(readmes).find(([path]) => path.endsWith(`/${example.path}/README.md`))?.[1]; +const Content = readme?.Content; +const headings = (readme?.getHeadings?.() ?? []).filter((heading) => heading.depth >= 2 && heading.depth <= 3); + +const codeUrl = `${config.repoUrl}/tree/${config.defaultBranch}/${example.path}`; +const contributeUrl = `${config.repoUrl}/blob/${config.defaultBranch}/CONTRIBUTING.md`; +const tags = [...new Set([example.type, ...example.components, ...example.products])]; +const badge = sourceLabel(example); +--- + + + + + + + {example.title} | Workday Examples Hub + + + + + + + +
+ +
+
+
+ + + +
+

+ + {example.section} + + / {example.id} +

+
+

{example.title}

+ {badge === "Workday" ? ( + + + Workday + + ) : ( + + + Community + + )} +
+

{example.description}

+
+ {tags.map((tag) => ( + + {tag} + + ))} +
+
+
+
+ {example.tutorial && ( + + Tutorial + + )} + + View code + +
+
+ +
+
+ {Content ? :

This example has no README yet.

} +
+ + {headings.length > 0 && ( + + )} +
+
+ +