Skip to content

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.

Step inputs support ${{ }} expressions that resolve at execution time:

NamespaceSyntaxDescription
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.

The context namespace provides metadata about the current run and the authenticated user. These are injected automatically — you don’t define them as inputs.

VariableDescriptionExample
context.createdByEmail of the user who triggered the runalice@example.com
context.userEmailUser email from the JWT (same as createdBy)alice@example.com
context.userIdUser ID from the identity provider283946191840477396
context.tenantIdTenant/organization ID283946191840477186
context.runIdUnique ID of this executiona1b2c3d4-e5f6-...
context.moldSlugSlug of the mold being executedcreate-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"

Creates a GitHub repository in an organization. Idempotent — if the repo already exists, it returns the existing repo instead of failing.

Required:

InputTypeDescription
namestringRepository name
ownerstringGitHub organization or user

Optional:

InputTypeDefaultDescription
descriptionstringShort description shown on GitHub
homepagestringURL with more information about the project
privatebooleanfalseWhether the repository is private
visibilitystringpublic or private (alternative to private field)
auto_initbooleanfalseCreate an initial commit with an empty README
gitignore_templatestring.gitignore template name (e.g. Go, Node, Python)
license_templatestringLicense key (e.g. mit, apache-2.0, gpl-3.0)

Features:

InputTypeDefaultDescription
has_issuesbooleantrueEnable the Issues tab
has_projectsbooleantrueEnable the Projects tab
has_wikibooleantrueEnable the Wiki tab
has_downloadsbooleantrueEnable the Downloads section
has_discussionsbooleanfalseEnable the Discussions tab
is_templatebooleanfalseMake this a template repository

Merge settings:

InputTypeDefaultDescription
allow_squash_mergebooleantrueAllow squash-merging pull requests
allow_merge_commitbooleantrueAllow merge commits on pull requests
allow_rebase_mergebooleantrueAllow rebase-merging pull requests
allow_auto_mergebooleanfalseAllow pull requests to merge automatically
delete_branch_on_mergebooleanfalseAutomatically delete head branches after merge
allow_update_branchbooleanfalseAllow updating pull request branches from the base
squash_merge_commit_titlestringPR_TITLE or COMMIT_OR_PR_TITLE
squash_merge_commit_messagestringPR_BODY, COMMIT_MESSAGES, or BLANK
merge_commit_titlestringPR_TITLE or MERGE_MESSAGE
merge_commit_messagestringPR_BODY, PR_TITLE, or BLANK

Custom properties:

InputTypeDescription
custom_propertiesobjectKey-value map of GitHub custom properties. Properties must be defined in the GitHub org first.
# Example: set custom properties on creation
custom_properties:
created-by: "${{ context.createdBy }}"
team: "platform"
environment: "production"
OutputDescription
html_urlBrowser URL (https://github.com/owner/name)
clone_urlGit clone URL
full_nameowner/name
nameRepository name
ownerOwner login
- 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 }}"

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.

Required:

InputTypeDescription
ownerstringGitHub organization or user
repostringRepository name

Optional: All fields from github.repo.create (description, homepage, merge settings, custom_properties, etc.) plus:

InputTypeDescription
default_branchstringChange the default branch (e.g. develop)
archivedbooleanArchive or unarchive the repository

Same as github.repo.create.

# 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: false

Deletes a GitHub repository. Not idempotent — fails if the repo does not exist.

InputTypeDescription
ownerstringGitHub organization or user
repostringRepository name
OutputDescription
deletedtrue
ownerOwner login
repoRepository name

Creates or updates a file in a repository. Idempotent — if the file already exists, it updates it using the file’s SHA.

Required:

InputTypeDescription
ownerstringGitHub organization or user
repostringRepository name
pathstringFile path in the repository (e.g. README.md, src/main.go)

Optional:

InputTypeDefaultDescription
contentstring""File content
messagestringAdd <path>Commit message
branchstringmainTarget branch
OutputDescription
pathFile path
shaFile SHA
html_urlBrowser URL to the file
- 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.

Replaces all topics on a repository. Idempotent.

InputTypeDescription
ownerstringGitHub organization or user
repostringRepository name
topicsstring[]List of topic strings

Topics can be provided as a YAML list or a comma-separated string:

# List syntax
topics: [golang, http-service, shoehorn]
# Or as an input reference
topics: "${{ inputs.topics }}"
OutputDescription
ownerOwner login
repoRepository name
topicsApplied topics

Creates a pull request. Idempotent — if an open PR already exists for the same head and base branches, it returns the existing PR.

Required:

InputTypeDescription
ownerstringGitHub organization or user
repostringRepository name
titlestringPull request title
headstringBranch with changes

Optional:

InputTypeDefaultDescription
basestringmainBranch to merge into
bodystring""Pull request description (supports markdown)
OutputDescription
html_urlBrowser URL to the PR
numberPR number
titlePR title

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).

Required (one of):

InputTypeDescription
ownerstringGitHub organization
repostringRepository name
team_slugstringDirect GitHub team slug (takes precedence if both are provided)
catalog_teamstringShoehorn catalog team ID or slug (resolved to a GitHub team — see below)

Optional:

InputTypeDefaultDescription
permissionstringpushPermission level: pull, push, admin, maintain, or triage

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:

  1. Group mapping with provider: github — if the team has a group mapping where the provider is "github", the idp_group_name is used as the GitHub team slug.
  2. Team metadata github_team_slug — if the team’s metadata contains a github_team_slug field, that value is used.
  3. 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.

OutputDescription
ownerOrganization
repoRepository name
team_slugResolved GitHub team slug
permissionApplied permission level

Direct GitHub team slug:

- id: add-team
action: github.team.add
inputs:
owner: "${{ inputs.owner }}"
repo: "${{ inputs.name }}"
team_slug: "backend-team"
permission: push

From Shoehorn catalog team:

- id: add-team
action: github.team.add
inputs:
owner: "${{ inputs.owner }}"
repo: "${{ inputs.name }}"
catalog_team: "${{ inputs.team }}"
permission: push

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.

Required:

InputTypeDescription
ownerstringGitHub organization or user
repostringRepository name
usernamestringGitHub username to invite

Optional:

InputTypeDefaultDescription
permissionstringpushPermission level: pull, push, admin, maintain, or triage
OutputDescription
ownerOrganization
repoRepository name
usernameInvited username
permissionApplied permission level
invitedtrue
- id: add-maintainer
action: github.collaborator.add
inputs:
owner: "${{ inputs.owner }}"
repo: "${{ inputs.name }}"
username: "${{ inputs.maintainer }}"
permission: maintain

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.

Required:

InputTypeDescription
service_idstringUnique identifier for the entity (slug format, e.g. my-api)
namestringDisplay name
typestringEntity type: service, library, website, api, team

Optional:

InputTypeDescription
descriptionstringShort description
lifecyclestringproduction, staging, experimental, deprecated
ownerstringOwning team slug
tagsstring[]List of tags
linksobject[]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.

OutputDescription
service_idEntity ID
nameEntity name
typeEntity type
registeredtrue
# 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: Grafana

  • When GITHUB_FORGE_ORGANIZATION is configured, the owner input on github.repo.create is overridden to the configured org. This prevents molds from targeting arbitrary organizations.
  • The secrets and env namespaces are blocked in sandboxed mode (external/untrusted molds). Only inputs, steps, and context are available.
  • All GitHub operations use the token configured in the Forge admin settings, not user tokens.