Scaffolder Actions
Scaffolder actions are the built-in operations available in mold steps that use action:. All GitHub actions talk to the GitHub API using the token configured in the Forge admin settings.
For how to use these in a mold, see Creating Molds.
Expression syntax
Section titled “Expression syntax”Step inputs support ${{ }} expressions that resolve at execution time:
| Namespace | Syntax | Description |
|---|---|---|
| inputs | ${{ inputs.name }} | User-provided form values |
| parameters | ${{ parameters.name }} | Alias for inputs |
| steps | ${{ steps.create-repo.output.html_url }} | Output from a previous step |
| context | ${{ context.createdBy }} | Run and user metadata (see below) |
| secrets | ${{ secrets.GITHUB_TOKEN }} | Secret values (blocked in sandboxed mode) |
| env | ${{ env.NODE_ENV }} | Environment variables (blocked in sandboxed mode) |
Type preservation: if an entire string is a single expression ("${{ inputs.private }}"), the original type is preserved (boolean, integer, map). When mixed with text ("prefix ${{ inputs.name }}"), values are coerced to strings.
Context variables
Section titled “Context variables”The context namespace provides metadata about the current run and the authenticated user. These are injected automatically — you don’t define them as inputs.
| Variable | Description | Example |
|---|---|---|
context.createdBy | Email of the user who triggered the run | alice@example.com |
context.userEmail | User email from the JWT (same as createdBy) | alice@example.com |
context.userId | User ID from the identity provider | 283946191840477396 |
context.tenantId | Tenant/organization ID | 283946191840477186 |
context.runId | Unique ID of this execution | a1b2c3d4-e5f6-... |
context.moldSlug | Slug of the mold being executed | create-repo-with-custom-properties |
Context variables are always available, even in sandboxed mode (they contain non-sensitive metadata only).
Common use case — track who created a resource:
steps: - id: create-repo action: github.repo.create inputs: name: "${{ inputs.name }}" owner: "${{ inputs.owner }}" custom_properties: created-by: "${{ context.createdBy }}" managed-by: "shoehorn"github.repo.create
Section titled “github.repo.create”Creates a GitHub repository in an organization. Idempotent — if the repo already exists, it returns the existing repo instead of failing.
Inputs
Section titled “Inputs”Required:
| Input | Type | Description |
|---|---|---|
name | string | Repository name |
owner | string | GitHub organization or user |
Optional:
| Input | Type | Default | Description |
|---|---|---|---|
description | string | Short description shown on GitHub | |
homepage | string | URL with more information about the project | |
private | boolean | false | Whether the repository is private |
visibility | string | public or private (alternative to private field) | |
auto_init | boolean | false | Create an initial commit with an empty README |
gitignore_template | string | .gitignore template name (e.g. Go, Node, Python) | |
license_template | string | License key (e.g. mit, apache-2.0, gpl-3.0) |
Features:
| Input | Type | Default | Description |
|---|---|---|---|
has_issues | boolean | true | Enable the Issues tab |
has_projects | boolean | true | Enable the Projects tab |
has_wiki | boolean | true | Enable the Wiki tab |
has_downloads | boolean | true | Enable the Downloads section |
has_discussions | boolean | false | Enable the Discussions tab |
is_template | boolean | false | Make this a template repository |
Merge settings:
| Input | Type | Default | Description |
|---|---|---|---|
allow_squash_merge | boolean | true | Allow squash-merging pull requests |
allow_merge_commit | boolean | true | Allow merge commits on pull requests |
allow_rebase_merge | boolean | true | Allow rebase-merging pull requests |
allow_auto_merge | boolean | false | Allow pull requests to merge automatically |
delete_branch_on_merge | boolean | false | Automatically delete head branches after merge |
allow_update_branch | boolean | false | Allow updating pull request branches from the base |
squash_merge_commit_title | string | PR_TITLE or COMMIT_OR_PR_TITLE | |
squash_merge_commit_message | string | PR_BODY, COMMIT_MESSAGES, or BLANK | |
merge_commit_title | string | PR_TITLE or MERGE_MESSAGE | |
merge_commit_message | string | PR_BODY, PR_TITLE, or BLANK |
Custom properties:
| Input | Type | Description |
|---|---|---|
custom_properties | object | Key-value map of GitHub custom properties. Properties must be defined in the GitHub org first. |
# Example: set custom properties on creationcustom_properties: created-by: "${{ context.createdBy }}" team: "platform" environment: "production"Outputs
Section titled “Outputs”| Output | Description |
|---|---|
html_url | Browser URL (https://github.com/owner/name) |
clone_url | Git clone URL |
full_name | owner/name |
name | Repository name |
owner | Owner login |
Example
Section titled “Example”- id: create-repo name: Create repository action: github.repo.create inputs: name: "${{ inputs.name }}" owner: "${{ inputs.owner }}" private: true auto_init: true license_template: "mit" gitignore_template: "Go" delete_branch_on_merge: true allow_squash_merge: true allow_merge_commit: false allow_rebase_merge: false custom_properties: created-by: "${{ context.createdBy }}"github.repo.update
Section titled “github.repo.update”Updates settings on an existing repository. Useful for fields that require the repo to exist first (like default_branch) or for updating settings in a later step.
Inputs
Section titled “Inputs”Required:
| Input | Type | Description |
|---|---|---|
owner | string | GitHub organization or user |
repo | string | Repository name |
Optional: All fields from github.repo.create (description, homepage, merge settings, custom_properties, etc.) plus:
| Input | Type | Description |
|---|---|---|
default_branch | string | Change the default branch (e.g. develop) |
archived | boolean | Archive or unarchive the repository |
Outputs
Section titled “Outputs”Same as github.repo.create.
Example
Section titled “Example”# After creating a repo with auto_init, change the default branch- id: update-repo name: Configure repository action: github.repo.update inputs: owner: "${{ inputs.owner }}" repo: "${{ inputs.name }}" default_branch: "develop" delete_branch_on_merge: true allow_squash_merge: true allow_merge_commit: falsegithub.repo.delete
Section titled “github.repo.delete”Deletes a GitHub repository. Not idempotent — fails if the repo does not exist.
Inputs
Section titled “Inputs”| Input | Type | Description |
|---|---|---|
owner | string | GitHub organization or user |
repo | string | Repository name |
Outputs
Section titled “Outputs”| Output | Description |
|---|---|
deleted | true |
owner | Owner login |
repo | Repository name |
github.file.create
Section titled “github.file.create”Creates or updates a file in a repository. Idempotent — if the file already exists, it updates it using the file’s SHA.
Inputs
Section titled “Inputs”Required:
| Input | Type | Description |
|---|---|---|
owner | string | GitHub organization or user |
repo | string | Repository name |
path | string | File path in the repository (e.g. README.md, src/main.go) |
Optional:
| Input | Type | Default | Description |
|---|---|---|---|
content | string | "" | File content |
message | string | Add <path> | Commit message |
branch | string | main | Target branch |
Outputs
Section titled “Outputs”| Output | Description |
|---|---|
path | File path |
sha | File SHA |
html_url | Browser URL to the file |
Example
Section titled “Example”- id: create-readme action: github.file.create inputs: owner: "${{ inputs.owner }}" repo: "${{ inputs.name }}" path: "README.md" message: "docs: add project README" content: | # ${{ inputs.name }}
Created by ${{ context.createdBy }} via Shoehorn Forge.github.topics.set
Section titled “github.topics.set”Replaces all topics on a repository. Idempotent.
Inputs
Section titled “Inputs”| Input | Type | Description |
|---|---|---|
owner | string | GitHub organization or user |
repo | string | Repository name |
topics | string[] | List of topic strings |
Topics can be provided as a YAML list or a comma-separated string:
# List syntaxtopics: [golang, http-service, shoehorn]
# Or as an input referencetopics: "${{ inputs.topics }}"Outputs
Section titled “Outputs”| Output | Description |
|---|---|
owner | Owner login |
repo | Repository name |
topics | Applied topics |
github.pr.create
Section titled “github.pr.create”Creates a pull request. Idempotent — if an open PR already exists for the same head and base branches, it returns the existing PR.
Inputs
Section titled “Inputs”Required:
| Input | Type | Description |
|---|---|---|
owner | string | GitHub organization or user |
repo | string | Repository name |
title | string | Pull request title |
head | string | Branch with changes |
Optional:
| Input | Type | Default | Description |
|---|---|---|---|
base | string | main | Branch to merge into |
body | string | "" | Pull request description (supports markdown) |
Outputs
Section titled “Outputs”| Output | Description |
|---|---|
html_url | Browser URL to the PR |
number | PR number |
title | PR title |
github.team.add
Section titled “github.team.add”Adds a GitHub team to a repository with a permission level. Idempotent — re-adding a team that already has access updates the permission level.
The team can be specified in two ways: directly with a GitHub team slug, or by referencing a Shoehorn catalog team (which resolves to a GitHub team automatically).
Inputs
Section titled “Inputs”Required (one of):
| Input | Type | Description |
|---|---|---|
owner | string | GitHub organization |
repo | string | Repository name |
team_slug | string | Direct GitHub team slug (takes precedence if both are provided) |
catalog_team | string | Shoehorn catalog team ID or slug (resolved to a GitHub team — see below) |
Optional:
| Input | Type | Default | Description |
|---|---|---|---|
permission | string | push | Permission level: pull, push, admin, maintain, or triage |
Catalog team resolution
Section titled “Catalog team resolution”When catalog_team is provided (and team_slug is not), the action fetches the team from the Shoehorn catalog API and resolves the GitHub team slug in this order:
- Group mapping with
provider: github— if the team has a group mapping where the provider is"github", theidp_group_nameis used as the GitHub team slug. - Team metadata
github_team_slug— if the team’s metadata contains agithub_team_slugfield, that value is used. - Fallback — the catalog team slug is used as-is as the GitHub team slug (works when teams are named the same in both systems).
To set up the mapping, add a group mapping with provider: github to your team in the Shoehorn admin, or set github_team_slug in the team’s metadata.
Outputs
Section titled “Outputs”| Output | Description |
|---|---|
owner | Organization |
repo | Repository name |
team_slug | Resolved GitHub team slug |
permission | Applied permission level |
Examples
Section titled “Examples”Direct GitHub team slug:
- id: add-team action: github.team.add inputs: owner: "${{ inputs.owner }}" repo: "${{ inputs.name }}" team_slug: "backend-team" permission: pushFrom Shoehorn catalog team:
- id: add-team action: github.team.add inputs: owner: "${{ inputs.owner }}" repo: "${{ inputs.name }}" catalog_team: "${{ inputs.team }}" permission: pushgithub.collaborator.add
Section titled “github.collaborator.add”Adds an individual collaborator to a repository. Sends an invitation if the user is not already a collaborator. Idempotent — re-inviting updates the permission level.
Inputs
Section titled “Inputs”Required:
| Input | Type | Description |
|---|---|---|
owner | string | GitHub organization or user |
repo | string | Repository name |
username | string | GitHub username to invite |
Optional:
| Input | Type | Default | Description |
|---|---|---|---|
permission | string | push | Permission level: pull, push, admin, maintain, or triage |
Outputs
Section titled “Outputs”| Output | Description |
|---|---|
owner | Organization |
repo | Repository name |
username | Invited username |
permission | Applied permission level |
invited | true |
Example
Section titled “Example”- id: add-maintainer action: github.collaborator.add inputs: owner: "${{ inputs.owner }}" repo: "${{ inputs.name }}" username: "${{ inputs.maintainer }}" permission: maintaincatalog.entity.register
Section titled “catalog.entity.register”Registers a new entity in the Shoehorn catalog by posting a manifest to the API. If an entity with the same service_id already exists, it is updated (upsert).
This action requires the SHOEHORN_API_URL environment variable to be set (defaults to http://api:8080 in Docker). The authenticated user’s JWT is forwarded automatically.
Inputs
Section titled “Inputs”Required:
| Input | Type | Description |
|---|---|---|
service_id | string | Unique identifier for the entity (slug format, e.g. my-api) |
name | string | Display name |
type | string | Entity type: service, library, website, api, team |
Optional:
| Input | Type | Description |
|---|---|---|
description | string | Short description |
lifecycle | string | production, staging, experimental, deprecated |
owner | string | Owning team slug |
tags | string[] | List of tags |
links | object[] | List of links, each with name, url, and optional icon |
Supported link icons: GitHub, GitLab, Grafana, ArgoCD, Kubernetes, Datadog, Slack, Jira, Jenkins, Docker, AWS, Terraform, Prometheus, Docs, and more.
Outputs
Section titled “Outputs”| Output | Description |
|---|---|
service_id | Entity ID |
name | Entity name |
type | Entity type |
registered | true |
Example
Section titled “Example”# Register the newly created repo as a catalog entity- id: register-entity action: catalog.entity.register inputs: service_id: "${{ inputs.name }}" name: "${{ inputs.name }}" type: service description: "A Go service created via Forge" lifecycle: experimental owner: "${{ inputs.team }}" tags: - golang - shoehorn-managed links: - name: GitHub url: "${{ steps.create-repo.output.html_url }}" icon: GitHub - name: Grafana url: "https://grafana.internal/d/${{ inputs.name }}" icon: GrafanaSecurity notes
Section titled “Security notes”- When
GITHUB_FORGE_ORGANIZATIONis configured, theownerinput ongithub.repo.createis overridden to the configured org. This prevents molds from targeting arbitrary organizations. - The
secretsandenvnamespaces are blocked in sandboxed mode (external/untrusted molds). Onlyinputs,steps, andcontextare available. - All GitHub operations use the token configured in the Forge admin settings, not user tokens.