Reusable GitHub Actions blocks and workflows for fast shipping teams
BuildSpace gives you three layers of CI/CD automation:
- Workflows: plug-and-play release pipelines. Point a workflow at your repo, provide a few secrets, and you're done. Most teams only need this.
- Blocks: compatibility recipes that combine setup, builds, packaging, and publication. Their existing inputs and output names remain available.
- Primitives: single-operation actions with no release policy or toolchain setup. Use them to compose a pipeline without adopting a block's other behavior.
- Quick Start
- Prerequisites
- Primitives
- Workflows
- Immutable Image Publisher
- Package Stage and Promote
- Rust Service Release
- TypeScript Service Release
- TypeScript Monorepo Release
- Go Binary Release
- Swift Stage and Promote
- Swift Release
- Package Release
- Package PR Build
- Swift Package PR Build
- Check README
- Update Documentation
- Update Skills
- Blocks (Composite Actions)
- check-pr-label
- check-readme
- generate-release-info
- determine-publish-version
- create-github-release
- detect-changed-packages
- bump-npm-version
- bump-monorepo-versions
- publish-github-packages
- publish-npm-packages
- rust-build
- typescript-build
- sync-crates-version
- update-tap
- publish-crates
- publish-npm
- publish-github-package
- comment-on-pr
- update-docs
- update-skills
- Architecture
- Contributing
- License
| I have a... | Use this workflow | Trigger |
|---|---|---|
| Container image for Kargo promotion | publish-image |
Push to main or a protected hotfix branch, after CI |
| Internal npm package on GitHub Packages, with any internal Rust crates built beside it | package-stage + package-promote |
Push to main; promotion by dispatch and approval |
| Public npm package on npmjs, staged and promoted the same way | package-stage + package-promote with registry: https://registry.npmjs.org |
Push to main; promotion by dispatch and approval |
Swift macOS .pkg, staged on staging Macs and promoted to production and Jamf |
swift-stage + swift-promote |
Push to main; promotion by dispatch and approval |
| SwiftPM library, released by tags | swift-stage + swift-promote with library: true |
Push to main; promotion by dispatch and approval |
macOS .pkg PR build (no binary) |
pkg-release-pr |
Every PR commit |
Swift macOS .pkg (PR build previews) |
swift-pkg-pr |
Every PR commit |
| Any project — check if README is current | check-readme |
Every PR |
| Any project — update skills docs on release | update-skills |
Release (via caller) |
For packages, follow Package Stage and Promote, including its pack-script contract. For Swift executables and libraries, follow Swift Stage and Promote. Each guide has the staging and promotion callers, required permissions, and approval configuration.
Merge to main to stage a build. Promote that stored build after approval;
neither step uses a PR release label or an AI API key. The production candidate
is prepared during staging, so promotion does not rebuild it.
The workflows below remain available for compatibility. They still use their existing label rules; do not remove a caller's labels before migrating its release path. The rule and caller inventory tracks the remaining consumers and removal conditions.
| Existing release path | Compatibility reference |
|---|---|
| Rust binaries and crates.io publishing | Rust Service Release |
| Single TypeScript / JavaScript package | TypeScript Service Release |
| Independently versioned TypeScript packages | TypeScript Monorepo Release |
| Go binaries | Go Binary Release |
Swift macOS .pkg direct release |
Swift Release |
Payload/scripts-only macOS .pkg |
Package Release |
| GitHub-only releases and Makefile dylibs | Compatibility rules |
Package stage/promote does not replace standalone Go/Rust binary releases or crates.io publishing. Swift stage/promote does not build Makefile dylibs or payload-only packages. Those callers need a replacement before retirement.
Add to any repo's .github/workflows/ci.yaml:
name: CI
on:
pull_request:
jobs:
check-readme:
uses: photon-hq/buildspace/.github/workflows/check-readme.yaml@main
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}This check runs on pull requests independently of release or promotion.
Examples using @main pick up every BuildSpace change the moment it merges.
Pin a reviewed commit of main instead, as the stage and promote examples do:
uses: photon-hq/buildspace/.github/workflows/package-stage.yml@<reviewed-commit-sha>Get the current commit with git ls-remote https://github.com/photon-hq/buildspace refs/heads/main. Moving the pin is a pull request in your repository, so each BuildSpace change is reviewed there, and a stage workflow's dry run tests it, before it ships. BuildSpace has no releases or tags of its own: merging to main is the release. The old v1.x tags and releases were deleted, so a reference to one no longer resolves; pin a commit instead.
Legacy direct-release wrappers still reference nested blocks at @main.
Pinning only a legacy wrapper does not freeze those dependencies; keep the
compatibility blocks available until their consumers migrate.
Package and Swift stage/promote workflows do not use AI API keys. AI checks and
legacy direct-release workflows use OPENAI_API_KEY; update-docs and
update-skills use ANTHROPIC_API_KEY. Add only the secrets your workflow needs:
| Secret | Needed for | Where to get it |
|---|---|---|
OPENAI_API_KEY |
AI checks and legacy direct-release workflows | platform.openai.com/api-keys |
ANTHROPIC_API_KEY |
Update Documentation, Update Skills | console.anthropic.com/settings/keys |
NPM_TOKEN |
Legacy token-based npm publishing; modern npm stage/promote uses trusted publishing | npmjs.com/settings/tokens |
CARGO_REGISTRY_TOKEN |
Legacy crates.io publishing | crates.io/settings/tokens |
APP_ID |
Update Documentation and Update Skills (required), release workflows (optional, for protected branches) | GitHub App settings |
APP_PRIVATE_KEY |
Update Documentation and Update Skills (required), release workflows (optional, for protected branches) | GitHub App settings |
Add these in your repository's Settings > Secrets and variables > Actions, or configure organization secrets with access granted to the calling repository. Pass the required secrets explicitly. GitHub supports secret inheritance only when the caller and reusable workflow are within the same organization or enterprise. Callers outside that boundary must use explicit secret mappings, as shown below.
GitHub Packages publishing does not need another stored secret. It uses the caller repository's short-lived GITHUB_TOKEN; grant the reusable-workflow job packages: write when publish-github-packages is enabled.
For stage/promote callers, main is staging. Production receives an approved
staging build's stored candidate. The release and prerelease PR labels are
not required or consulted by these workflows.
Label authorization remains only in the compatibility rules. Those rules differ between workflows and must stay intact for their remaining callers. Do not remove GitHub prerelease metadata, registry channels, or production-dependency checks when removing an unused PR label.
Workflows that create releases or push commits need contents: write.
Modern version calculation reads PR titles and descriptions and needs
pull-requests: read; legacy label inspection also needs it. Workflows that
post comments need pull-requests: write. GitHub Packages publishing needs
packages: write. Each workflow section below lists its required permissions.
Ready-to-use release pipelines. Each workflow composes the lower-level blocks internally - you don't need to know about individual blocks unless you're building a custom pipeline.
.github/workflows/publish-image.yml builds and attests the caller's exact commit
once, or reuses an existing image after verifying its original attestation.
Promoting to another environment does not call this workflow.
Call it as a job after required source CI, pinned to a reviewed commit:
publish:
needs: [test]
uses: photon-hq/buildspace/.github/workflows/publish-image.yml@<reviewed-commit-sha>
permissions:
contents: read
packages: read
attestations: write
id-token: write
with:
image: example-service
tag: main-${{ github.sha }}
dockerfile: Dockerfile
aws-role: arn:aws:iam::<account>:role/<service-publish-role>
secrets:
build-secrets: |
NODE_AUTH_TOKEN=${{ github.token }}- ECR must enforce immutability for
main-*andhotfix-*tags. Photon's existing Terraform usesIMMUTABLE_WITH_EXCLUSIONwith only the exact floatingmaintag excluded; never widen that exception tomain*orhotfix-*. This registry setting prevents another writer from replacing a tag between the lookup and push. Provision it before adopting the publisher. - Source repositories own tests, hotfix branch/review policy and production dependency checks. Main/hotfix tags must identify the exact event commit.
- Optional inputs configure context, platform, runner, environment, region and uncached build stages. All other behavior is shared. A matrix can call it for several independent images.
build-argsadds non-secret build arguments.GIT_SHAis always the event commit and cannot be replaced.submodulesis passed to the checkout, for a repository whose image source lives in a pinned submodule.- For submodules in private repositories of the same owner, also set
submodule-repositoriesand passapp-idandapp-private-key. The job token reads only the calling repository, so the checkout then uses a GitHub App token limited to reading the calling repository and the listed ones. The App must be installed on all of them. The token is not persisted in the checkout and is not passed toprepareor to the build. prepareruns a script from the caller in the checkout, for a build context the repository generates (for example a patched copy of a submodule). It is trusted code from the caller's reviewed commit, in the same sense as the Dockerfile, and is not a sandbox. It runs before AWS credentials are configured, with the job token and the OIDC request credentials cleared.- Every image carries
org.opencontainers.image.createdset to the committer time of the event commit, in UTC. Without it the image inherits the label of its base image, or falls back to the build time of its newest layer, which a fully cached build shares with the previous image. A pipeline that orders images by creation time then follows source order, and a rerun of an older commit does not become the newest image. - The image is pushed by digest, attested, and only then tagged. A tag therefore
never exists without its attestation: a pipeline that discovers images by tag
cannot pick up an unattested one, and a run that failed to attest leaves
nothing for the next attempt to trip over. The tag is added by putting the
same manifest bytes back with the expected digest, so the registry refuses a
tag for any other manifest. The publishing role needs
ecr:BatchGetImageas well asecr:PutImage. - The caller's repository identity and configured environment remain the AWS
OIDC subject. The reusable workflow becomes the attestation signer:
photon-hq/buildspace/.github/workflows/publish-image.yml. Verifiers must check that signer plus the caller repository, source ref and source SHA. legacy-signer-workflowpermits reuse of an existing image from a reviewed predecessor. It does not overwrite or re-attest the image. Missing attestations and ECR access errors fail the job.- For private Git dependencies fetched during the build, set
app-token-repositoriesand passapp-idandapp-private-key. The workflow mints a GitHub App token limited to reading those repositories and mounts it as the BuildKit secret named byapp-token-secret(defaultgithub_token). A token minted in the caller cannot be passed in: job outputs that contain a secret are dropped. - Supply only the BuildKit secrets needed by the Dockerfile. The workflow returns
digest; use the per-job result for matrix builds, not a combined matrix output.
Local validation: actionlint .github/workflows/publish-image.yml and python3 -m unittest discover -s test
(requires PyYAML 6.0.3).
.github/workflows/package-stage.yml and .github/workflows/package-promote.yml publish
private packages to GitHub Packages, or public packages to npmjs, the way publish-image and
Kargo ship images: every main commit is built and tested once, and production
receives exactly the files that staging tested. Internal Rust crates built from
the same commit are staged and promoted with them.
Each main push computes the build's version X.Y.Z from the pull requests merged
since the latest production release (see Versions below), then packs the
caller's packages twice from the same checkout: a staging build
X.Y.Z-staging.<run id>.<attempt>, published under the staging dist-tag, and
the production candidate X.Y.Z. Both are attested and attached to
a GitHub prerelease, <repository>-staging-<staging version>, with a
release-manifest.json holding their checksums and source commit. Promotion
publishes a build's stored candidates under latest after an environment
reviewer approves; nothing is rebuilt.
# .github/workflows/publish-staging.yml
name: Publish staging
on:
push:
branches: [main]
jobs:
stage:
uses: photon-hq/buildspace/.github/workflows/package-stage.yml@<reviewed-commit-sha>
permissions:
contents: write
packages: write
actions: read
attestations: write
id-token: write
pull-requests: read
with:
ci-workflow: ci.yml
verify: pnpm verify
pack: pnpm release:pack
paths: packages/adapter scripts/pack.mjs # optional: what counts toward the version
crates: acme-adapter # optional: Rust crates released with the packages
downstream: api web
secrets:
APP_ID: ${{ secrets.APP_ID }}
APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}# .github/workflows/promote.yml
name: Promote to production
on:
workflow_dispatch:
inputs:
staging-version:
description: Staging build to promote; blank promotes the current staging tag
default: ''
jobs:
promote:
uses: photon-hq/buildspace/.github/workflows/package-promote.yml@<reviewed-commit-sha>
permissions:
contents: write
packages: write
actions: read
attestations: read
# id-token: write # required when registry: https://registry.npmjs.org
with:
staging-version: ${{ inputs.staging-version }}These were npm-stage.yml and npm-promote.yml before crates joined them. A caller
moving off the old names changes both uses: lines and pins in one commit; its
next main push stages a build that package-promote.yml can promote, since builds
signed by the old stage workflow can't be.
To promote, run the promote workflow from main (Actions, or
gh workflow run promote.yml -f staging-version=1.4.0-staging.123456789.1). Its
first job shows what will be published, its version and the changes that version
was computed from, then the run waits for the production environment's
reviewers. After approval it publishes the candidates, creates the vX.Y.Z tag and Release on the
source commit, and moves latest.
- The
packscript is the caller's. It runs twice, withRELEASE_CHANNEL(stagingorproduction),RELEASE_VERSION(the build'sX.Y.Z),RELEASE_SUFFIX(-staging.<run id>.<attempt>, or empty) andPACK_DESTINATION. It must write one.tgzper package toPACK_DESTINATION, each versioned$RELEASE_VERSION$RELEASE_SUFFIX, and test what it packed. It must leave tracked files unchanged; the job fails otherwise.verifyruns once before both packs. Both run withNODE_AUTH_TOKENable to read packages. - Another private registry. When the lockfile also resolves packages from a
registry other than GitHub Packages, pass a read-only token for it as the
INSTALL_TOKENsecret and use it ininstall, for exampleinstall: pnpm install --frozen-lockfile --config.//registry.example/:_authToken="$INSTALL_TOKEN". Only the install script sees it;verifyandpackdo not. - npm-only repositories. Set
setup-pnpm: falseandinstall: npm ci. Node supplies npm; the workflow skips pnpm's package-manager-field check. Existing callers still set up their pinned pnpm by default, andbun-versioncontinues to select Bun in its place. - Several packages from one repository are staged and promoted together, all
at the build's version, so dependencies between them pin
$RELEASE_VERSION. They are published in dependency order, and the build is named afterpackage(default@<owner>/<repository>), which must be one of them.tag-prefixoverrides the staging tag prefix. - Packages of one repository released separately each get their own call
to the stage workflow, in one run, with their own
package,tag-prefix,production-tag-prefixandartifact-name, and their own promote workflow. A workflow that promotes several of them in one run, in dependency order, gives each promote call its ownartifact-nameas well. Give each its ownpathstoo, usually the paths its push trigger filters on, so a change to one package, or to a service beside them, does not version the others. The calls share the run's staging suffix, so a later one (needs:the earlier) can depend on the staging version the earlier one publishes. - A repository whose
vX.Y.Ztags already name something else, such as a service that has its own releases, setsproduction-tag-prefixto<name>-vin both workflows. Its packages are then released as<name>-vX.Y.Z, and promotion neither needs nor touches thevX.Y.Ztags. Use the same value in the stage and the promote caller: a build staged under one prefix is refused under another. Crates always usevX.Y.Z, so a build withcratescannot set a prefix. - Trust boundaries. Only the build job runs caller code, and it can read but
not write. A separate job attests the files and publishes them, after
rebuilding every manifest claim (source commit, run, scope, versions, tags,
order) from the files themselves and its own run. The staging
job uses the
environmentinput (defaultstaging); restrict that environment tomain. Promotion requiresmain, verifies each candidate's checksum and its attestation (signerphoton-hq/buildspace/.github/workflows/package-stage.yml, the caller repository,refs/heads/mainand the source commit), and refuses unless theproductionenvironment exists with required reviewers or the run is the caller'strusted-actordispatching (below). - Promoting the build of a service image. A repository that also ships an
image passes
image-tag(main-<sha>) in place ofstaging-version. Promotion then takes the newest build staged at that commit or an ancestor of it, which is the build the image was made with when only commits that change the packages are staged. It refuses when a stage run for a commit in between published nothing, so a failed or unfinished build is never skipped over. Ahotfix-<baseline>-<sha>tag releases nothing: packages are staged frommain. - Nothing to publish. When every candidate is already published with the same contents and its production release exists, the run ends after the read-only job, without the environment. Identical packages keep the tag of the commit that first released them, so a later build with unchanged packages promotes cleanly.
- A release controller as the approver.
trusted-actornames an actor whose ownworkflow_dispatchis the approval, for example the GitHub App a release controller dispatches with after a person promoted in it. GitHub states who dispatched a run and who started each attempt, so no input can claim either, and the actor must be both. Such a run skips the reviewer requirement and publishes without the environment; every other check still applies. A run anyone else dispatches, and any re-run by or of another actor's run, waits for the environment's reviewers as before. Set it only where that actor's dispatches are already gated by people, and keep its token scoped to the repository. - A public package on npmjs sets
registry: https://registry.npmjs.organdpackage, its scoped name such as@acme/app, in both callers, and grantsid-token: writeto both calls. The staging build and the promoted candidates are published through npm trusted publishing, with provenance and no token, by npm 11.21.1: trusted publishing manages dist-tags from npm 11.21.0. On npmjs, give the package a trusted publisher for each caller workflow file, for examplepublish.ymlwith environmentstagingandpromote.ymlwith environmentproduction, and allow bothnpm publishandnpm dist-tagon each. Leave the environment off the promote publisher if atrusted-actorpromotes, since its runs have none. npm scans a new version for a few minutes before it serves it, so each publish waits up to 30 minutes for the version to appear before it checks its integrity and moves a tag. The dependency rules below apply to the package's own scope, and a build staged for one registry is refused under the other. - A Bun repository sets
bun-versionin the stage caller, which sets up Bun in place of pnpm, and its owninstall, such asbun install --frozen-lockfile. - Promotion refuses a candidate that is not newer than
latestor is already published with other contents, and one whose same-scopedependenciesoroptionalDependenciesare not exact stable published versions (or packages promoted with it), or whose same-scope peer ranges no published stable version satisfies. Promote dependencies first. - Rust crates named in
cratesare staged and promoted with the packages. Cargo fetches internal crates from Git, not a registry, so a crate's release is the build's tag: consumers pin the production tag and the crate's own exact version, as the release notes show, for example:Pin the staging tag to try a staging build. Afteracme-error = { version = "=0.1.0", git = "https://github.com/acme/error", tag = "v0.3.0" }
verify, the build job runscargo package --locked --no-verify --exclude-lockfilefor each crate (Cargo 1.87+, clean checkout); the.craterecords its source commit and is attested and attached to both releases. Cargo reads a crate's version from its source at the tag, so the version stays the one inCargo.toml; the build's bump says how far it must move. Promotion refuses a crate whose packaged files changed since the latest production release unless its version rose by at least the bump's level (a patch when no change counted), for example0.4.0after0.3.2for a minor bump. It also refuses one whose normal or build dependencies from the owner's GitHub repositories, inherited ones included, are not pinned to avX.Y.Ztag with an exact=X.Y.Zversion. An npm package still names each release, and the job token reads only the calling repository, so a crate whose build fetches another private repository cannot be staged yet. - Versions. A separate read-only job computes the build's version before
any caller code runs;
package.jsonversions are not read. It starts from the highest stable<production-tag-prefix>X.Y.Ztag and reads every first-parent commit onmainsince then that touchespaths(default: the whole repository). Each commit counts once per merged pull request, by the pull request's title and description, whether it was squashed, merged or rebased; a commit pushed without one counts by its own message.- The largest change decides the bump, by
Conventional Commits:
type!:or aBREAKING CHANGE:footer is major,featminor, and anything else a patch, including a title that does not parse, which the run warns about. Below 1.0 a breaking change releases1.0.0. - Merges between promotions do not add up: from
v1.26.0, afixstages1.26.1, a laterfeat1.27.0, a laterdocsstill1.27.0, and promoting that build makesv1.27.0the next starting point. - A
Release-As: X.Y.Zline in a description raises the version to at leastX.Y.Z, for an intended major or a line's first version; one at or below the latest release is ignored with a warning. A line with no release yet starts at0.1.0, and one with no change since its release keeps its version. - The staging release, the version job's summary and the promotion summary
list each change with the release it asks for. The version job reads pull
requests, so the stage call needs
pull-requests: read. - A package released earlier with byte-identical contents is left as it is.
When a build's contents changed but no change inside
pathscounted, the staging run warns that it cannot be promoted: widenpathsor merge aRelease-As.
- The largest change decides the bump, by
Conventional Commits:
- Moving to computed versions. Builds staged before it keep the version
their
package.jsongave them and still promote. A caller moving its pins grantspull-requests: readto the stage call, passes the paths its push trigger filters on aspaths, and packs at$RELEASE_VERSIONinstead of thepackage.jsonversion. Wherepackage.jsonis ahead of the latest release with an unpromoted bump, the moving pull request's description carriesRelease-As: <that version>so the bump is kept. - Retries. Every publish step accepts its own earlier work: a version already
published with the same integrity, a tag on the same commit, an existing
release missing assets. Anything else stops before writing. Channel tags never
move to an older version; a temporary
candidate-<run>-<attempt>dist-tag is left only by a failed run. ci-workflowwaits up toci-wait-minutes(default 20) for that workflow's push run on the exact commit to succeed.dry-run: trueverifies and packs without publishing and is the only mode allowed outsidemain, so a pull request branch can dispatch it.downstreamrepositories receive aninternal-package-publisheddispatch after each publication, using theAPP_IDandAPP_PRIVATE_KEYGitHub App.
Local validation: node --test test/package-release.test.mjs test/release-version.test.mjs (after
npm ci --prefix .github/package-release) and python3 -m unittest discover -s test
(the crate packaging test runs when cargo is installed).
Action: .github/blocks/check-production-dependencies
- Shared by Kargo source-repository preflights and manual checks.
- Pass
image-tag(main-<sha>orhotfix-<baseline>-<sha>) orsource-ref. - Audits what that commit has at its root: a pnpm workspace
(
pnpm-workspace.yaml), a Cargo workspace (Cargo.tomlwith its committedCargo.lock), or both. A commit with neither fails. - Reads the manifests and lockfiles from Git; never changes pins, installs service dependencies, or runs Cargo. Only the checker's locked public dependencies are installed, with lifecycle scripts disabled.
- Supports single-document pnpm v9 lockfiles and pnpm 12 multi-document lockfiles (toolchain graph followed by the application graph). Both graphs are audited; malformed or unrecognized document layouts fail.
- npm: all direct/transitive
@photon-hq/*registry packages must use exact stable, published, non-deprecated versions. Dev, optional and peer dependencies are included. A workspace package'speerDependenciesmay instead be a range without a prerelease bound, such as>=0.25.0, which the version its lockfile installs must satisfy; that version is audited like any other pin. Valid localworkspace:links are source in the same commit; their package dependencies are still audited. - Cargo: internal crates are the ones fetched from the owner's GitHub
repositories, and their release is a
vX.Y.Ztag (see Package Stage and Promote). Every such crate inCargo.lock, transitive ones included, must resolve from avX.Y.Ztag at an exact stable version; arev, a branch or a staging tag fails. The tag must be a published release, not a draft or prerelease, and must still point at the commitCargo.lockresolved. Each workspace package's own declarations (normal, dev, build and target dependencies, inherited ones included) must pin that tag with an exact=X.Y.Zversion, the line the release notes print. Path dependencies are source in the same commit; their dependencies are still audited. - Uses the caller's token with
contents: readandpackages: read. The calling repository needs read access to its internal packages. No Buf or AWS token. - Verifying a crate's release reads its repository, which the job token cannot
do for another private repository. Pass
crates-token, a token withcontents: readin the repositories the internal crates come from. Without it those crates are reported as Unknown and the check fails. A source with no internal crates needs no such token. - Reports the package/version/results tables in the Actions summary and fails on blockers or unknown registry or release results. Kargo runs it once in production-ready.
steps:
- uses: photon-hq/buildspace/.github/blocks/check-production-dependencies@<reviewed-commit-sha>
with:
image-tag: ${{ inputs.image-tag }}
github-token: ${{ github.token }}For a source whose internal crates live in other private repositories, mint a token limited to reading them:
steps:
- uses: actions/create-github-app-token@<reviewed-commit-sha>
id: crates
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: |
error
permission-contents: read
- uses: photon-hq/buildspace/.github/blocks/check-production-dependencies@<reviewed-commit-sha>
with:
image-tag: ${{ inputs.image-tag }}
github-token: ${{ github.token }}
crates-token: ${{ steps.crates.outputs.token }}Pin the action to a reviewed commit. For manual checks, replace image-tag with
source-ref: main. A standalone checkout step is unnecessary: the action checks
out the calling repository with full history and reads the chosen ref from Git.
Local validation: npm ci --prefix .github/blocks/check-production-dependencies
and node --test .github/blocks/check-production-dependencies/*.test.mjs (Node 24+).
File: .github/workflows/rust-service-release.yaml
Compatibility workflow. Keep its existing release policy until the caller migrates. See the rules and remaining callers.
Complete release pipeline for Rust services: checks PR labels, generates version and release notes with AI, builds binaries for Linux (x86_64), macOS (ARM64), and Windows, syncs version across all workspace crates, publishes to crates.io, and creates a GitHub Release with attached binaries.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
service-name |
string | Yes | — | Display name for the service |
binary-name |
string | Yes | — | Name of the binary from Cargo.toml |
commit-user-name |
string | No | photon-release[bot] |
Git user name for release commits |
commit-user-email |
string | No | photon-release[bot]@users.noreply.github.com |
Git user email for release commits |
binary-path |
string | No | "" |
Path to crate directory (e.g., crates/client) |
crates |
string | No | [] |
JSON array of crate paths to publish in dependency order |
build-env |
string | No | "" |
Compile-time env vars (e.g., BASE_URL=https://...) |
prerelease |
boolean | No | false |
Select RC mode for an enabled release (adds -rc.N) |
release |
boolean | No | false |
Enable the legacy release; checker failures still stop it |
dry-run |
boolean | No | false |
Publisher dry run only; version commits, GitHub Releases, and configured Homebrew tap commits and pushes can still occur |
| Secret | Required | Description |
|---|---|---|
OPENAI_API_KEY |
Yes | For AI-powered versioning and release notes |
CARGO_REGISTRY_TOKEN |
No | crates.io API token (required for publishing) |
jobs:
release:
uses: photon-hq/buildspace/.github/workflows/rust-service-release.yaml@main
permissions:
contents: write
pull-requests: read
with:
service-name: enva
binary-name: enva
binary-path: crates/client
crates: '["crates/shared", "crates/client"]'
build-env: "API_URL=https://api.example.com"
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}File: .github/workflows/typescript-service-release.yaml
Compatibility workflow. New package callers use Package Stage and Promote. See the rules and remaining callers before removing labels.
Complete release pipeline for a single TypeScript/JavaScript package: checks PR labels, generates version and release notes with AI, bumps package.json, creates a GitHub Release, publishes to npm, and can optionally publish the same package to GitHub Packages.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
service-name |
string | Yes | — | Display name for the service |
source-sha |
string | No | "" |
Inspect and tag this exact full commit SHA; omission preserves existing behavior |
commit-user-name |
string | No | photon-release[bot] |
Git user name for release commits |
commit-user-email |
string | No | photon-release[bot]@users.noreply.github.com |
Git user email for release commits |
bun-version |
string | No | latest |
Bun version to use |
npm-tag |
string | No | latest |
Package tag for npmjs.org and GitHub Packages (e.g., latest, beta, next) |
no-npm-publish |
boolean | No | false |
Skip npmjs.org publishing |
publish-github-packages |
boolean | No | false |
Also publish to npm.pkg.github.com; requires an @owner/package name and caller packages: write |
working-directory |
string | No | . |
Directory containing package.json |
build-command |
string | No | bun run build |
Build command to run |
publish-command |
string | No | npm publish |
Publisher command; target-specific registry and publish flags are appended |
prerelease |
boolean | No | false |
Select RC mode; see the compatibility entry rules |
release |
boolean | No | false |
Enable the legacy release; checker failures still stop it |
dry-run |
boolean | No | false |
Publisher dry run only; version commits and GitHub Releases can still occur |
use-oidc |
boolean | No | false |
Opt in to npm OIDC Trusted Publishing (requires caller id-token: write + a trusted publisher). Falls back to NPM_TOKEN. Default keeps least-privilege token publishing |
| Secret | Required | Description |
|---|---|---|
OPENAI_API_KEY |
Yes | For AI-powered versioning and release notes |
NPM_TOKEN |
No | npm auth token; used as the fallback when OIDC Trusted Publishing isn't configured |
jobs:
release:
uses: photon-hq/buildspace/.github/workflows/typescript-service-release.yaml@main
permissions:
contents: write
pull-requests: read
# id-token: write # only needed when use-oidc: true (npm Trusted Publishing)
# packages: write # only needed when publish-github-packages: true
with:
service-name: notebooklm-kit
build-command: "npm run build"
# use-oidc: true # opt in to npm OIDC Trusted Publishing
# publish-github-packages: true
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}Enabling OIDC Trusted Publishing (opt-in): set
use-oidc: true, addid-token: writeto the caller'spermissions, configure a trusted publisher for the package on npmjs.com, and ensure the runner has npm ≥ 11.5.1. Callers that don't setuse-oidcare completely unaffected — they keepcontents: readleast-privilege and publish viaNPM_TOKENexactly as before.
Exact-source releases: pass source-sha when a merge receipt or another trusted
workflow has selected a specific source commit. The checkout, version analysis,
release notes, and tag use that commit. The shared workflow returns version,
tag, and source-sha outputs. A retry reuses an existing release for that source
rather than computing a second version, and an existing tag pointing elsewhere
is rejected. The main-branch version bump remains separate from the pinned source;
package publishing applies the computed version to its pinned checkout.
Omitting source-sha retains existing caller behavior. Source-aware caller changes
must be deployed after these workflow and composite-action changes reach main.
Run node --test test/source-release.test.mjs for the exact-source regression tests.
Publishing to GitHub Packages (opt-in): set
publish-github-packages: trueand addpackages: writeto the caller'spermissions. The package'snamemust be scoped to the repository owner (for example,@acme/app). BuildSpace authenticates with the automaticGITHUB_TOKEN, so no PAT or additional secret is required. Leaveno-npm-publishasfalseto publish to both registries, or set it totruefor GitHub Packages only.
File: .github/workflows/typescript-monorepo-release.yaml
Compatibility workflow. New package callers use Package Stage and Promote. Its shared build version differs from this workflow's independent package versions; see the migration rules.
Complete release pipeline for TypeScript/JavaScript monorepos with independently-versioned packages. Detects which packages changed since the last release, topologically sorts them by dependency order, uses a single AI call to determine all versions and generate combined release notes, bumps each package.json, creates a GitHub Release with a release/YYYY-MM-DD.N tag, publishes all changed packages to npm in dependency order, and can optionally mirror them to GitHub Packages.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
service-name |
string | Yes | — | Display name for the monorepo |
commit-user-name |
string | No | photon-release[bot] |
Git user name for release commits |
commit-user-email |
string | No | photon-release[bot]@users.noreply.github.com |
Git user email for release commits |
packages |
string | Yes | — | JSON array of packages: [{"name":"pkg","path":"packages/pkg"}] |
bun-version |
string | No | latest |
Bun version to use |
npm-tag |
string | No | latest |
Package tag for npmjs.org and GitHub Packages |
publish-github-packages |
boolean | No | false |
Also publish changed packages to npm.pkg.github.com; every name must use the repository owner's scope and caller must grant packages: write |
build-command |
string | No | bun run build |
Per-package build command (ignored if root-build-command is set) |
root-build-command |
string | No | "" |
Build once at repo root (e.g., turbo build) |
include-dependents |
boolean | No | false |
Also release downstream dependents of changed packages |
prerelease |
boolean | No | false |
Select RC mode; see the compatibility entry rules |
release |
boolean | No | false |
Enable the legacy release; checker failures still stop it |
dry-run |
boolean | No | false |
Publisher dry run only; version commits and GitHub Releases can still occur |
| Secret | Required | Description |
|---|---|---|
OPENAI_API_KEY |
Yes | For AI-powered versioning and release notes |
NPM_TOKEN |
Yes | npm authentication token |
APP_ID |
No | GitHub App ID (for pushing to protected branches) |
APP_PRIVATE_KEY |
No | GitHub App private key (for pushing to protected branches) |
jobs:
release:
uses: photon-hq/buildspace/.github/workflows/typescript-monorepo-release.yaml@main
permissions:
contents: write
pull-requests: read
packages: write
with:
service-name: acme
packages: |
[
{"name":"@acme/app","path":"packages/app"},
{"name":"@acme/client","path":"packages/client"},
{"name":"@acme/create-app","path":"packages/create-app"}
]
root-build-command: "turbo build"
include-dependents: true
publish-github-packages: true
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}GitHub Packages uses the automatic GITHUB_TOKEN; no additional secret is needed. Every opted-in package must be scoped to the repository owner, such as @acme/client.
PR merged with "release" label
│
▼
┌─────────────┐ ┌───────────────────┐ ┌─────────────────┐
│ Check Labels │────▶│ Detect Changed │────▶│ Bump Versions │
│ │ │ Packages (topo- │ │ (single AI call │
│ │ │ sorted by deps) │ │ for all pkgs) │
└─────────────┘ └───────────────────┘ └────────┬────────┘
│
┌────────┴────────┐
│ │
▼ ▼
┌──────────────┐ ┌─────────────┐
│GitHub Release │ │npm + GitHub │
│(combined tag) │ │Pkg Publish │
└──────────────┘ └─────────────┘
File: .github/workflows/go-service-release.yaml
Compatibility workflow. Keep its existing release policy until a binary stage/promote path replaces it. See the rules and remaining callers.
Complete release pipeline for Go binaries: checks PR labels, generates version and release notes with AI, cross-compiles for Linux (x64/ARM64), macOS (ARM64), and Windows (x64), and creates a GitHub Release with attached binaries.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
service-name |
string | Yes | — | Display name for the service |
binary-name |
string | Yes | — | Output binary name |
commit-user-name |
string | No | photon-release[bot] |
Git user name for optional Homebrew tap commits |
commit-user-email |
string | No | photon-release[bot]@users.noreply.github.com |
Git user email for optional Homebrew tap commits |
go-version |
string | No | stable |
Go version to use |
build-flags |
string | No | "" |
Additional go build flags |
ldflags |
string | No | -s -w |
Linker flags |
prerelease |
boolean | No | false |
Select RC mode; see the compatibility entry rules |
release |
boolean | No | false |
Enable the legacy release; checker failures still stop it |
| Secret | Required | Description |
|---|---|---|
OPENAI_API_KEY |
Yes | For AI-powered versioning and release notes |
jobs:
release:
uses: photon-hq/buildspace/.github/workflows/go-service-release.yaml@main
permissions:
contents: write
pull-requests: read
with:
service-name: my-go-tool
binary-name: my-go-tool
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}.github/workflows/swift-stage.yml and .github/workflows/swift-promote.yml release
Swift macOS .pkg packages, and SwiftPM libraries, the way
Package Stage and Promote releases npm packages.
For a package, every main commit is built once, into a staging package that
staging Macs run and a production candidate from the same checkout, and
promotion releases that candidate without rebuilding it. A library builds
nothing: stage tags the commit with a manifest-only prerelease, and promotion
tags vX.Y.Z on it (see Libraries below). Versions follow the same rule
(see Versions there) under vX.Y.Z tags.
Each main push, after ci-workflow passes on that commit:
- A read-only job computes
X.Y.Zfrom the pull requests merged since the latestvX.Y.Z. - A macOS job builds the package twice from one checkout and build directory:
the staging build at
X.Y.Z-staging.<run id>.<attempt>and the production candidate atX.Y.Z. Each compiles with a.envofVERSION,BUILD_ENVIRONMENT=production,RELEASE_CHANNEL(stagingorproduction) andSECRET_ENV_VARS, andpkgbuildpackages it at its version. - A separate job attests both packages and publishes the GitHub prerelease
vX.Y.Z-staging.<run id>.<attempt>. It holds the staging<package-name>.pkg, the candidate as<package-name>-X.Y.Z.pkg.candidateandrelease-manifest.json. GitGate and the Astrolabe self-updater install the first.pkgof the newest release on the prerelease channel, so staging Macs pick up every main commit, and the candidate's name keeps them off it.
Promotion, dispatched from main (a dispatch from another branch fails), proves
where the candidate came from and waits for the production environment's
reviewers. After approval it tags
vX.Y.Z on the source commit, publishes the release with the candidate as
<package-name>.pkg, and uploads that package to Jamf Pro when jamf-url is
set, building the uploader from the same pinned jamf-package-updater commit.
Nothing is rebuilt.
# .github/workflows/release.yml
name: Stage
on:
push:
branches: [main]
workflow_dispatch:
inputs:
dry_run:
type: boolean
default: true
jobs:
stage:
uses: photon-hq/buildspace/.github/workflows/swift-stage.yml@<reviewed-commit-sha>
permissions:
contents: write
actions: read
attestations: write
id-token: write
pull-requests: read
with:
package-name: macrocosm-astrolabe
identifier: codes.photon.macrocosm-astrolabe
ci-workflow: ci.yml # optional: a push workflow that must pass first
private-deps: true # optional: a dependency is one of the owner's private repositories
dry-run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
secrets:
APP_ID: ${{ secrets.APP_ID }}
APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}
SECRET_ENV_VARS: ${{ secrets.SECRET_ENV_VARS }}# .github/workflows/promote.yml
name: Promote to production
on:
workflow_dispatch:
inputs:
staging-version:
description: Staging build to promote; blank promotes the newest
default: ''
jobs:
promote:
uses: photon-hq/buildspace/.github/workflows/swift-promote.yml@<reviewed-commit-sha>
permissions:
contents: write
actions: read
attestations: read
with:
package-name: macrocosm-astrolabe
staging-version: ${{ inputs.staging-version }}
jamf-url: ${{ vars.JAMF_URL }} # optional
private-deps: true # optional: a dependency is one of the owner's private repositories
secrets:
APP_ID: ${{ secrets.APP_ID }}
APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}
JAMF_CLIENT_ID: ${{ secrets.JAMF_CLIENT_ID }}
JAMF_CLIENT_SECRET: ${{ secrets.JAMF_CLIENT_SECRET }}- Inputs.
package-name,identifier,scripts-path,payload-path,resource-bundles,entitlementsandprivate-deps(with theAPP_IDandAPP_PRIVATE_KEYsecrets) work as in Swift Release.paths,ci-workflow,ci-wait-minutes,environment(defaultstaging) anddry-runwork as inpackage-stage. The promote call names the samepackage-nameandlibrary, and setsprivate-deps(with the same secrets) when a dependency is one of the owner's private repositories. - Toolchain. The build installs the Swift toolchain that the package's
committed
.swift-versionnames, or the latest Swift release when it has none. swiftly rewrites.swift-versionwhen it selects a toolchain, so the build restores the committed file before building. - Dependencies. Production depends only on production. Promotion reads the
source commit's
Package.resolvedandPackage.swiftfrom GitHub, never running them, and checks every dependency fetched from one of the owner's GitHub repositories. A package is held to the pins of its committedPackage.resolved, which the build resolved from and could not change: each must be a stableX.Y.Z(not a branch, a bare revision or a staging version) whosevX.Y.ZorX.Y.Ztag is a published, non-prerelease release still naming the pinned commit. A library, whose consumers resolve its requirements themselves, and a package that commits noPackage.resolvedare held to the requirements inPackage.swiftinstead: each must be a version requirement written as string literals, with no branch, revision or prerelease bound, and its lowest version must be such a release. Reading a private repository's releases takesprivate-deps. The reviewer's summary lists each release it checked: the pinned version fromPackage.resolved, or the lowest versionPackage.swiftrequires, which SwiftPM may resolve higher. - Libraries.
library: truebuilds nothing. Stage waits for CI, then tagsvX.Y.Z-staging.<run id>.<attempt>with a prerelease that holds only the manifest, and promotion tagsvX.Y.Zon the same commit. SwiftPM resolves only stable versions for a requirement such asfrom: "1.2.0", so a staging tag reaches only a consumer that pins it exactly. - The version in the binary. The staging build reports
X.Y.Z-staging.<run id>.<attempt>asVERSIONand the candidateX.Y.Z. Both keepBUILD_ENVIRONMENT=production, as release builds always have, so staging Macs' telemetry keeps its environment;RELEASE_CHANNELtells the two builds apart. - Staging Macs after a promotion. GitGate's prerelease channel installs the
most recently published release, and a promotion publishes
vX.Y.Z. A staging Mac running a later staging build of a GitGate package returns tovX.Y.Zuntil the next main push stages again. The Astrolabe self-updater compares versions and refuses that downgrade. - Promotion refuses a build whose tag no longer names its commit, whose
commit is not on
main, whose candidate fails its checksum or its attestation (signerphoton-hq/buildspace/.github/workflows/swift-stage.yml, the caller repository,refs/heads/mainand the source commit), whose version is not newer than the latestvX.Y.Z, or that depends on the owner's repositories other than through production releases (see Dependencies), and it refuses unless theproductionenvironment requires a reviewer. A build already released asvX.Y.Zfrom the same commit has nothing to publish. - Trust boundaries. Only the build job runs caller code, and it can read but
not write.
SECRET_ENV_VARSand the dependency token reach only its build step. Promotion's own read-only dependency token reaches only its resolve step, which runs no caller code. The staging publish job rebuilds every manifest claim from the files, its own run and the version job's output. - Moving from
swift-release. Thereleaseandprereleaselabels andOPENAI_API_KEYare no longer used. The first staged version follows the latest stablevX.Y.Z;-rc.Ntags are ignored, and a repository without a stable tag starts at0.1.0unless the moving pull request carriesRelease-As.
Local validation: node --test test/swift-release.test.mjs (Node 24+, after
npm ci --prefix .github/package-release) and
python3 -m unittest discover -s test -p 'test_swift*' (requires PyYAML 6.0.3).
File: .github/workflows/swift-release.yml
Compatibility workflow. New Swift callers use Swift Stage and Promote. See the rules and remaining callers before removing labels.
The label-driven pipeline that Swift Stage and Promote replaces. Complete release pipeline for macOS .pkg distribution packages: checks PR labels, generates version and release notes with AI, builds the Swift binary, creates a .pkg, creates a GitHub Release, and optionally uploads stable releases to Jamf Pro. A prerelease label or input creates an -rc.N GitHub pre-release and skips Jamf upload.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
package-name |
string | Yes | — | Name of the Swift binary / package |
identifier |
string | Yes | — | Package identifier (e.g., com.example.mytool) |
scripts-path |
string | No | "" |
Path to scripts directory with preinstall/postinstall scripts |
payload-path |
string | No | "" |
Path to additional payload directory whose contents mirror the install root |
resource-bundles |
string | No | "" |
Space-separated list of SPM resource bundle names to include in the .pkg |
entitlements |
string | No | "" |
Path to entitlements plist for ad-hoc codesigning the built binary |
private-deps |
boolean | No | false |
Mint a GitHub App token so SwiftPM can clone private/internal org dependencies |
prerelease |
boolean | No | false |
Enable an RC release, mark the GitHub release as prerelease, and skip Jamf |
release |
boolean | No | false |
Enable the legacy release; a prerelease input or label still selects RC mode |
jamf-url |
string | No | "" |
Jamf Pro instance URL (leave empty to skip Jamf upload) |
jamf-package-priority |
string | No | "" |
Package priority in Jamf Pro |
jamf-package-name |
string | No | "" |
Package name to match in Jamf Pro |
use-blacksmith |
boolean | No | false |
Use Blacksmith Linux runners for Linux jobs |
| Secret | Required | Description |
|---|---|---|
OPENAI_API_KEY |
Yes | For AI-powered versioning and release notes |
SECRET_ENV_VARS |
No | Compile-time env vars written to .env |
JAMF_CLIENT_ID |
No | Jamf Pro API client ID |
JAMF_CLIENT_SECRET |
No | Jamf Pro API client secret |
APP_ID |
No | GitHub App ID for private SwiftPM dependencies |
APP_PRIVATE_KEY |
No | GitHub App private key for private SwiftPM dependencies |
jobs:
release:
uses: photon-hq/buildspace/.github/workflows/swift-release.yml@main
with:
package-name: my-tool
identifier: com.example.my-tool
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}File: .github/workflows/pkg-release.yml
Compatibility workflow. A payload-only package needs its own replacement; Swift stage/promote has no payload-only mode. See the rules and remaining callers.
Release pipeline for macOS .pkg distribution packages that don't contain a compiled binary. Packages payload files and scripts into a .pkg, creates a GitHub Release, and optionally uploads to Jamf Pro. Use this instead of swift-release when your package only delivers configuration files, LaunchDaemons, scripts, or other non-binary payload.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
package-name |
string | Yes | — | Name of the package |
identifier |
string | Yes | — | Package identifier (e.g., com.example.my-config) |
scripts-path |
string | No | "" |
Path to scripts directory with preinstall/postinstall scripts |
payload-path |
string | No | "" |
Path to payload directory whose contents mirror the install root |
use-blacksmith |
boolean | No | false |
Use Blacksmith Linux runners for Linux jobs |
jamf-url |
string | No | "" |
Jamf Pro instance URL (leave empty to skip Jamf upload) |
jamf-package-priority |
string | No | "" |
Package priority in Jamf Pro |
jamf-package-name |
string | No | "" |
Package name to match in Jamf Pro |
| Secret | Required | Description |
|---|---|---|
OPENAI_API_KEY |
Yes | For AI-powered versioning and release notes |
JAMF_CLIENT_ID |
No | Jamf Pro API client ID |
JAMF_CLIENT_SECRET |
No | Jamf Pro API client secret |
jobs:
release:
uses: photon-hq/buildspace/.github/workflows/pkg-release.yml@main
with:
package-name: my-config-pkg
identifier: com.example.my-config
payload-path: payload
scripts-path: scripts
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}File: .github/workflows/pkg-release-pr.yml
Builds a macOS .pkg (without compiling a binary) on every PR commit and reports status directly in the PR as a living comment. Same PR experience as swift-pkg-pr but for payload/scripts-only packages. If a build is already running when a new commit is pushed, the old run is automatically cancelled.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
package-name |
string | Yes | — | Name of the package |
identifier |
string | Yes | — | Package identifier (e.g., com.example.my-config) |
scripts-path |
string | No | "" |
Path to scripts directory with preinstall/postinstall scripts |
payload-path |
string | No | "" |
Path to payload directory whose contents mirror the install root |
use-blacksmith |
boolean | No | false |
Use Blacksmith Linux runners for notification jobs; the macOS package build stays on macos-26 |
# .github/workflows/pr.yml
name: PR
on:
pull_request:
jobs:
pkg:
uses: photon-hq/buildspace/.github/workflows/pkg-release-pr.yml@main
with:
package-name: my-config-pkg
identifier: com.example.my-config
payload-path: payload
scripts-path: scriptsFile: .github/workflows/swift-pkg-pr.yml
Builds a macOS .pkg on every PR commit and reports status directly in the PR as a living comment (updated in place, not spammy). If a build is already running when a new commit is pushed, the old run is automatically cancelled.
- Posts a "Building..." comment on the PR (or updates the existing one)
- Builds the Swift binary, creates a
.pkgversioned aspr.<PR#>.<run#> - Uploads the
.pkgas an Actions artifact (7-day retention) - Updates the PR comment to success (with artifact link) or failure (with log link)
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
package-name |
string | Yes | — | Name of the Swift binary / package |
identifier |
string | Yes | — | Package identifier (e.g., com.example.mytool) |
scripts-path |
string | No | "" |
Path to scripts directory with preinstall/postinstall scripts |
payload-path |
string | No | "" |
Path to additional payload directory whose contents mirror the install root |
resource-bundles |
string | No | "" |
Space-separated list of SPM resource bundle names to include in the .pkg |
entitlements |
string | No | "" |
Path to entitlements plist for ad-hoc codesigning the built binary |
private-deps |
boolean | No | false |
Mint a GitHub App token so SwiftPM can clone private/internal org dependencies |
use-blacksmith |
boolean | No | false |
Use Blacksmith Linux runners for notification jobs; the macOS Swift/package build stays on macos-26 |
| Secret | Required | Description |
|---|---|---|
SECRET_ENV_VARS |
No | Compile-time env vars written to .env |
APP_ID |
No | GitHub App ID for private SwiftPM dependencies |
APP_PRIVATE_KEY |
No | GitHub App private key for private SwiftPM dependencies |
# .github/workflows/pr.yml
name: PR
on:
pull_request:
jobs:
swift-pkg:
uses: photon-hq/buildspace/.github/workflows/swift-pkg-pr.yml@main
with:
package-name: my-tool
identifier: com.example.my-tool
secrets:
SECRET_ENV_VARS: ${{ secrets.SECRET_ENV_VARS }}File: .github/workflows/check-readme.yaml
Runs on every PR to verify that README.md is up to date with the changes being introduced. Uses AI to read the README and the changed files, then determines whether the documentation needs updating. Posts a PR comment explaining what's missing when the README is stale, and automatically removes the comment when the README is brought up to date.
Flags changes to public APIs, features, configuration, install steps, and environment variables. Ignores internal refactors, test-only changes, CI tweaks, and bug fixes that don't affect documented behavior.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
blocking |
boolean | No | false |
If true, fail the workflow when the README is outdated. If false, only post a warning comment. |
| Secret | Required | Description |
|---|---|---|
OPENAI_API_KEY |
Yes | For AI-powered README analysis |
name: CI
on:
pull_request:
jobs:
check-readme:
uses: photon-hq/buildspace/.github/workflows/check-readme.yaml@main
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}jobs:
check-readme:
uses: photon-hq/buildspace/.github/workflows/check-readme.yaml@main
with:
blocking: true
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}To enforce this as a required check, also add
check-readmeas a required status check in your branch protection rules (Settings > Branches > Branch protection rule).
File: .github/workflows/update-docs.yaml
Runs when a release is published. Uses Claude Code to analyze the changes between the current and previous release, then submits a PR to the documentation repository with any necessary updates. Focuses on API changes, new features, configuration changes, breaking changes, and deprecations.
The workflow generates a cross-repo GitHub App token from the caller-provided APP_ID and APP_PRIVATE_KEY secrets. It clones the target docs repository, updates the documentation from the release diff and notes, and opens a PR when changes exist.
AI may update authored guides through ordinary PR review. Existing target-repository rules determine edit scope and page placement. docs-path is a focus hint and may name a page or directory that does not exist yet. No extra directory allowlist, producer mapping, or approval gate is imposed.
For photon-hq/docs, the updater follows the existing Mintlify conventions and preserves producer-owned references, navigation, and locks. Dependency installation retains pnpm's normal CI behavior without forcing SDK upgrades or requiring a lockfile from targets that do not have one. The generator uses the repository's current checks: pnpm test, pnpm docs:validate, and pnpm docs:links. Other target repositories follow their own validation instructions.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
service-name |
string | No | Repository name | Name of the service being released |
docs-path |
string | Yes | — | Documentation focus hint; existing target-repository rules determine edit scope and page placement |
docs-repo |
string | No | photon-hq/docs |
Target documentation repository (owner/repo) |
docs-branch |
string | No | main |
Base branch of the docs repo |
use-blacksmith |
boolean | No | false |
Use Blacksmith Linux runners instead of ubuntu-latest |
Configure ANTHROPIC_API_KEY, APP_ID, and APP_PRIVATE_KEY in the calling repository, or as organization secrets available to it. Pass all three secrets explicitly, as shown below. For inheritance restrictions, see Required Secrets. The GitHub App must have access to the target docs repository. The workflow uses its credentials to mint a short-lived token scoped to that repository.
name: Update Docs on Release
on:
release:
types: [published]
jobs:
update-docs:
uses: photon-hq/buildspace/.github/workflows/update-docs.yaml@main
with:
docs-path: advanced-kits/imessage
secrets:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
APP_ID: ${{ secrets.APP_ID }}
APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}File: .github/workflows/update-skills.yaml
Runs when a release is published. Uses Claude Code to analyze the changes between the current and previous release, then submits a PR to the skills repository (photon-hq/skills) with updated SKILL.md files. The caller controls whether skills actually need updating via the skills-need-update input — when false, the workflow exits early without running AI or cloning the skills repo.
The workflow generates a cross-repo GitHub App token from the caller-provided APP_ID and APP_PRIVATE_KEY secrets. It clones the target skills repository, updates the skills from the release diff and notes, and opens a PR when changes exist.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
skills-need-update |
boolean | No | false |
Whether this release introduces changes that should be reflected in the skills repo. When false, the workflow exits early. |
service-name |
string | No | Repository name | Name of the service being released |
skills-path |
string | Yes | — | Path within the skills repo that this service maps to (e.g., skills/buildspace-ci-cd or skills/imessage) |
skills-repo |
string | No | photon-hq/skills |
Target skills repository (owner/repo) |
skills-branch |
string | No | main |
Base branch of the skills repo |
release-tag |
string | No | "" |
Override the release event's tag |
release-notes |
string | No | "" |
Override the release event's notes |
use-blacksmith |
boolean | No | false |
Use Blacksmith Linux runners instead of ubuntu-latest |
Configure ANTHROPIC_API_KEY, APP_ID, and APP_PRIVATE_KEY in the calling repository, or as organization secrets available to it. Pass all three secrets explicitly, as shown below. For inheritance restrictions, see Required Secrets. The GitHub App must have access to the target skills repository. The workflow uses its credentials to mint a short-lived token scoped to that repository.
name: Update Skills on Release
on:
release:
types: [published]
jobs:
update-skills:
uses: photon-hq/buildspace/.github/workflows/update-skills.yaml@main
with:
skills-need-update: true
skills-path: skills/my-service
secrets:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
APP_ID: ${{ secrets.APP_ID }}
APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}Package and Swift staging compute versions from merged changes, without release labels or AI version selection. Production receives the stored candidate after approval, without rerunning the caller's build.
flowchart LR
M[Merge to main] --> V[Compute version from merged changes]
V --> B[Prepare staging build and production candidate]
B --> S[Attest and publish staging build]
S --> A[Approve selected staging build]
A --> P[Publish stored production candidate]
For images, the immutable publisher builds and attests the exact commit; Kargo handles promotion separately.
The older label-driven workflows still generate versions and publish directly. Their compatibility rules remain in force until their callers migrate. Deleting a gate would enable direct publishing; it would not turn that workflow into staging and promotion.
Individual building blocks that the workflows above are assembled from. Use these directly when you need a custom pipeline.
Expand all blocks
Path: .github/blocks/collect-release-context/action.yaml
Reads release data from an existing full checkout with tags. It requires Bash, Git and Python 3, but no credentials, model or publishing access.
Pass the required release-tag and optional release-notes. The action returns
version (one leading v removed), unchanged release_tag and release_notes,
previous_tag (empty for the first release), reviewers, and changes_diff.
It keeps creation-date tag order and filters team entries out of CODEOWNERS.
The diff ends at the selected tag, even when the checkout's HEAD is newer. Its
first-release base is Git's empty tree. Missing tags fail. The diff is capped at
100000 characters, with NUL bytes removed and a notice when truncated.
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: photon-hq/buildspace/.github/blocks/collect-release-context@main
id: context
with:
release-tag: ${{ github.event.release.tag_name }}
release-notes: ${{ github.event.release.body }}Path: .github/blocks/check-pr-label/action.yaml
Inspects the requested PR labels and returns boolean flags. The caller decides what those flags mean; the block does not publish or authorize a release itself. The compatibility workflows still consume it, but modern stage/promote workflows do not.
On a push or manual run, it paginates the commit's associated pull requests and
selects the merged result matching that commit and base branch. API errors and
ambiguous owning matches fail without emitting the labels output. If no merged
PR matches, the labels output includes every requested lowercase key set to
false, including when no pull request is associated with the commit.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
labels |
string | Yes | — | JSON array of labels to check (e.g., ["needs-review", "documentation"]) |
| Output | Type | Description |
|---|---|---|
labels |
string | JSON-encoded object with lowercase keys and boolean values (e.g., {"needs-review": true, "documentation": false}) |
- uses: photon-hq/buildspace/.github/blocks/check-pr-label@main
id: labels
with:
labels: '["documentation"]'
- if: fromJSON(steps.labels.outputs.labels).documentation
run: echo "Documentation label found"Path: .github/blocks/check-readme/action.yaml
Uses AI to read the project's README.md alongside the PR's changed files and determine whether the documentation needs updating. Returns a boolean verdict and a one-sentence explanation. This is the block that powers the Check README workflow.
The README and skills checks read every changed-file page and compare the result with the PR's file count. An incomplete or capped list fails the check. Both checks require a complete verdict and a nonempty reason; empty or malformed model output fails without emitting a clean result or clearing an existing warning.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
openai-api-key |
secret | Yes | — | OpenAI API key |
| Output | Type | Description |
|---|---|---|
up-to-date |
boolean | true if the README appears current, false if it likely needs changes |
explanation |
string | One-sentence AI explanation of the verdict |
- uses: actions/checkout@v5
- uses: photon-hq/buildspace/.github/blocks/check-readme@main
id: readme
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
- if: steps.readme.outputs.up-to-date == 'false'
run: echo "README needs updating — ${{ steps.readme.outputs.explanation }}"Path: .github/blocks/generate-release-info/action.yaml
Generates a semantic version number and AI-written release notes by analyzing commit history since the last release. Combines determine-publish-version with release note generation in one step.
Both AI steps use Codex's read-only safety strategy and receive the supplied API key explicitly. They return text without editing the checkout or changing the runner's sudo access. The notes step uses a separate temporary Codex home so it does not reuse version detection's proxy configuration.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
service-name |
string | Yes | — | Service name (used in release notes) |
prerelease |
boolean | No | false |
Append -rc.N suffix to version |
openai-api-key |
secret | Yes | — | OpenAI API key |
| Output | Type | Description |
|---|---|---|
version |
string | Determined version (e.g., 1.2.3 or 1.2.3-rc.5) |
release_notes |
string | AI-generated release notes in markdown |
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: photon-hq/buildspace/.github/blocks/generate-release-info@main
id: info
with:
service-name: my-service
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
- run: |
echo "Version: ${{ steps.info.outputs.version }}"
echo "Notes: ${{ steps.info.outputs.release_notes }}"Path: .github/blocks/determine-publish-version/action.yaml
Standalone action for determining the next semantic version using AI analysis of commits. Lighter-weight alternative to generate-release-info when you don't need release notes.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
prerelease |
boolean | No | false |
Append -rc.N suffix to version |
openai-api-key |
secret | Yes | — | OpenAI API key |
| Output | Type | Description |
|---|---|---|
version |
string | Determined version (e.g., 1.2.3) |
previous-version |
string | Previous version before this release |
- uses: photon-hq/buildspace/.github/blocks/determine-publish-version@main
id: version
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
- run: echo "Bumping from ${{ steps.version.outputs.previous-version }} to ${{ steps.version.outputs.version }}"Path: .github/blocks/create-github-release/action.yaml
Creates a GitHub Release with optional artifact attachments.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
version |
string | Yes | — | Version number (e.g., 1.2.3) |
title |
string | Yes | — | Release title |
notes |
string | No | "" |
Release notes in markdown |
prerelease |
boolean | No | false |
Mark as prerelease |
draft |
boolean | No | false |
Create as draft |
tag-prefix |
string | No | v |
Prefix for git tag (e.g., v1.2.3) |
artifact-pattern |
string | No | "" |
Pattern to match artifacts to attach |
| Output | Type | Description |
|---|---|---|
url |
string | URL of the created release |
tag |
string | Created tag name (e.g., v1.2.3) |
- uses: photon-hq/buildspace/.github/blocks/create-github-release@main
with:
version: "1.2.3"
title: "My Service v1.2.3"
notes: |
## What's New
- Added awesome feature
artifact-pattern: "my-binary-*"Path: .github/blocks/detect-changed-packages/action.yaml
Detects which monorepo packages have changed since the last GitHub Release and returns them in topological (dependency) order. Optionally includes downstream dependents.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
packages |
string | Yes | — | JSON array: [{"name":"pkg","path":"packages/pkg"}] |
include-dependents |
boolean | No | false |
Also include packages that depend on changed packages |
| Output | Type | Description |
|---|---|---|
changed |
JSON | Array of changed packages in topological order |
has-changes |
boolean | Whether any packages have changes |
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: photon-hq/buildspace/.github/blocks/detect-changed-packages@main
id: detect
with:
packages: '[{"name":"@acme/app","path":"packages/app"},{"name":"@acme/client","path":"packages/client"}]'
include-dependents: true
- if: steps.detect.outputs.has-changes == 'true'
run: echo "Changed packages: ${{ steps.detect.outputs.changed }}"Path: .github/blocks/bump-npm-version/action.yaml
Bumps package.json, commits, and pushes. Optional commit-user-name and
commit-user-email inputs configure the Git identity without changing authentication.
Omitting both preserves photon-release[bot] <photon-release[bot]@users.noreply.github.com>.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
version |
string | Yes | — | Version to set in package.json |
working-directory |
string | No | . |
Directory containing package.json |
github-token |
secret | Yes | — | GitHub token (fallback) |
app-id |
secret | No | "" |
GitHub App ID (for protected branches) |
app-private-key |
secret | No | "" |
GitHub App private key |
commit-user-name |
string | No | photon-release[bot] |
Git user name for release commits |
commit-user-email |
string | No | photon-release[bot]@users.noreply.github.com |
Git user email for release commits |
- uses: photon-hq/buildspace/.github/blocks/bump-npm-version@main
with:
version: "1.2.3"
github-token: ${{ github.token }}
commit-user-name: "Acme Release Bot"
commit-user-email: "release@acme.example"The TypeScript service, TypeScript monorepo, Rust service, and Go service workflows accept the same optional identity inputs. Existing callers need no changes.
Path: .github/blocks/bump-monorepo-versions/action.yaml
Determines versions for all changed monorepo packages using a single AI call, bumps each package.json, commits, and pushes.
Use optional commit-user-name and commit-user-email inputs to change the Git identity for these commits.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
changed-packages |
string | Yes | — | JSON array from detect-changed-packages output |
commit-user-name |
string | No | photon-release[bot] |
Git user name for release commits |
commit-user-email |
string | No | photon-release[bot]@users.noreply.github.com |
Git user email for release commits |
service-name |
string | Yes | — | Service name for commit messages |
prerelease |
boolean | No | false |
Append -rc.N suffix to versions |
openai-api-key |
secret | Yes | — | OpenAI API key |
github-token |
secret | Yes | — | GitHub token (fallback) |
app-id |
secret | No | "" |
GitHub App ID (for protected branches) |
app-private-key |
secret | No | "" |
GitHub App private key |
| Output | Type | Description |
|---|---|---|
versions |
JSON | Object mapping package names to new versions |
release-notes |
string | Combined AI-generated release notes |
- uses: photon-hq/buildspace/.github/blocks/bump-monorepo-versions@main
id: bump
with:
changed-packages: ${{ steps.detect.outputs.changed }}
service-name: acme
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
github-token: ${{ github.token }}Path: .github/blocks/publish-github-packages/action.yaml
Builds and publishes changed monorepo packages to the GitHub Packages npm registry in dependency order. It authenticates with a GitHub token, validates that every package uses the repository owner's scope, and verifies each published version.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
changed-packages |
string | Yes | — | JSON array of packages in topological order |
bun-version |
string | No | latest |
Bun version |
node-version |
string | No | 24 |
Node.js version |
tag |
string | No | latest |
Package tag |
build-command |
string | No | bun run build |
Per-package build command (ignored if root-build-command is set) |
root-build-command |
string | No | "" |
Build once at repo root |
dry-run |
boolean | No | false |
Validate package contents without publishing |
github-token |
secret | Yes | — | GitHub token with packages: write |
- uses: photon-hq/buildspace/.github/blocks/publish-github-packages@main
with:
changed-packages: ${{ steps.detect.outputs.changed }}
root-build-command: "turbo build"
github-token: ${{ github.token }}Path: .github/blocks/publish-npm-packages/action.yaml
Builds and publishes multiple monorepo packages to npm in dependency order. Supports both per-package builds and a single root build command.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
changed-packages |
string | Yes | — | JSON array of packages in topological order |
bun-version |
string | No | latest |
Bun version |
node-version |
string | No | 24 |
Node.js version |
tag |
string | No | latest |
npm dist-tag |
build-command |
string | No | bun run build |
Per-package build command (ignored if root-build-command is set) |
root-build-command |
string | No | "" |
Build once at repo root (e.g., turbo build) |
dry-run |
boolean | No | false |
Run npm publish --dry-run |
npm-token |
secret | Yes | — | npm authentication token |
- uses: photon-hq/buildspace/.github/blocks/publish-npm-packages@main
with:
changed-packages: ${{ steps.detect.outputs.changed }}
root-build-command: "turbo build"
npm-token: ${{ secrets.NPM_TOKEN }}Path: .github/blocks/rust-build/action.yaml
Builds a Rust binary for a specific target platform.
| Target | OS |
|---|---|
x86_64-unknown-linux-gnu |
Linux (x64) |
aarch64-apple-darwin |
macOS (ARM64) |
x86_64-pc-windows-msvc |
Windows (x64) |
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
binary-name |
string | Yes | — | Name of the binary (from Cargo.toml) |
binary-path |
string | No | "" |
Path to crate directory |
target |
string | Yes | — | Target triple |
build-env |
string | No | "" |
Compile-time env vars for .env file |
| Output | Type | Description |
|---|---|---|
artifact-name |
string | Name of the built artifact |
artifact-path |
string | Path to the artifact file |
- uses: photon-hq/buildspace/.github/blocks/rust-build@main
with:
binary-name: my-cli
target: x86_64-unknown-linux-gnuPath: .github/blocks/typescript-build/action.yaml
Builds a TypeScript project using Bun.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
bun-version |
string | No | latest |
Bun version |
working-directory |
string | No | . |
Directory containing package.json |
build-command |
string | No | bun run build |
Build command |
- uses: photon-hq/buildspace/.github/blocks/typescript-build@main
with:
build-command: "npm run build"Path: .github/blocks/sync-crates-version/action.yaml
Sets a single version across all workspace crates using cargo-edit, commits, and pushes.
Use optional commit-user-name and commit-user-email inputs to change the Git identity for these commits.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
version |
string | Yes | — | Version to set across all crates |
commit-changes |
boolean | No | true |
Whether to commit and push |
commit-user-name |
string | No | photon-release[bot] |
Git user name for release commits |
commit-user-email |
string | No | photon-release[bot]@users.noreply.github.com |
Git user email for release commits |
github-token |
secret | Yes | — | GitHub token for pushing |
| Output | Type | Description |
|---|---|---|
updated-crates |
JSON | Array of crate names that were updated |
- uses: photon-hq/buildspace/.github/blocks/sync-crates-version@main
with:
version: "1.2.3"
github-token: ${{ github.token }}Path: .github/blocks/update-tap/action.yaml
Updates a Homebrew formula's version and checksums, then commits and pushes to the tap.
Use optional commit-user-name and commit-user-email inputs to change the Git identity for that commit.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
formula-name |
string | Yes | — | Name of the formula to update |
version |
string | Yes | — | Version to set |
tap-repo |
string | Yes | — | Homebrew tap repository |
app-id |
secret | Yes | — | GitHub App ID with access to the tap |
app-private-key |
secret | Yes | — | GitHub App private key |
commit-user-name |
string | No | photon-release[bot] |
Git user name for release commits |
commit-user-email |
string | No | photon-release[bot]@users.noreply.github.com |
Git user email for release commits |
Path: .github/blocks/publish-crates/action.yaml
Publishes workspace crates to crates.io in dependency order with retry logic and rate limit handling.
Order matters. List dependencies before dependents.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
crates |
string | Yes | — | JSON array of crate paths in publish order |
dry-run |
boolean | No | false |
Run cargo publish --dry-run |
cargo-registry-token |
secret | Yes | — | crates.io API token |
- uses: photon-hq/buildspace/.github/blocks/publish-crates@main
with:
crates: '["crates/shared", "crates/client"]'
cargo-registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}Path: .github/blocks/publish-npm/action.yaml
Publishes a single package to npm (installs dependencies, builds, publishes). Tries npm OIDC Trusted Publishing first (when the calling job has id-token: write), then falls back to token-based publishing with npm-token.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
bun-version |
string | No | latest |
Bun version |
node-version |
string | No | 24 |
Node.js version |
working-directory |
string | No | . |
Directory containing package.json |
build-command |
string | No | bun run build |
Build command |
tag |
string | No | latest |
npm dist-tag |
dry-run |
boolean | No | false |
Run npm publish --dry-run |
publish-command |
string | No | npm publish |
Publisher command; registry, tag, access, and dry-run flags are appended |
npm-token |
secret | No | — | npm token; used only as a fallback when OIDC Trusted Publishing is unavailable or fails |
- uses: photon-hq/buildspace/.github/blocks/publish-npm@main
with:
build-command: "npm run build"
npm-token: ${{ secrets.NPM_TOKEN }}- OIDC Trusted Publishing (preferred): when the calling job has
id-token: write, the action publishes tokenlessly with provenance. Requires a configured trusted publisher for the package on npmjs.com and npm ≥ 11.5.1 on the runner. - Token fallback: if OIDC is unavailable (no
id-token: write) or fails, the action publishes withnpm-token. This is the default behavior today — keep providingNPM_TOKENso publishing works until OIDC is fully set up.
Path: .github/blocks/publish-github-package/action.yaml
Builds and publishes one owner-scoped npm package to GitHub Packages. It uses an explicit registry flag so a package can be published to npmjs.org and GitHub Packages in the same release even when publishConfig.registry is present, then verifies the exact version is visible.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
bun-version |
string | No | latest |
Bun version |
node-version |
string | No | 24 |
Node.js version |
working-directory |
string | No | . |
Directory containing package.json |
build-command |
string | No | bun run build |
Build command |
tag |
string | No | latest |
Package tag |
dry-run |
boolean | No | false |
Validate package contents without publishing |
publish-command |
string | No | npm publish |
Publisher command; registry, tag, and dry-run flags are appended |
github-token |
secret | Yes | — | GitHub token with packages: write |
- uses: photon-hq/buildspace/.github/blocks/publish-github-package@main
with:
build-command: "npm run build"
github-token: ${{ github.token }}Path: .github/blocks/comment-on-pr/action.yaml
Posts, updates, or removes a marked comment on a pull request. With a
comment-key, it searches every comment page and updates the first matching
comment. Other markers and later duplicates remain unchanged. Without a pull
request payload, it skips without making API calls.
Without a key, the default present state creates an unmarked comment.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
message |
string | No | "" |
Comment body for the default present state (markdown supported) |
comment-key |
string | No | "" |
Unique key to find and update an existing comment |
state |
string | No | present |
present creates or updates; absent removes the first matching comment and requires a nonempty key |
- uses: photon-hq/buildspace/.github/blocks/comment-on-pr@main
with:
comment-key: my-build-status
message: |
## Build Status
All checks passed for commit ${{ github.sha }}Path: .github/blocks/update-docs/action.yaml
Uses Claude Code to analyze release changes and submit a PR to a documentation repository. Clones the docs repo, runs the Claude Code CLI to identify and make documentation updates, then creates a PR if changes were made. Expects a pre-generated github-token with write access to the docs repo (the update-docs workflow generates this token from the caller-provided App secrets).
Authored guide updates follow the target repository's existing rules without an added directory allowlist. Generated references stay with their declared producers. The action keeps pnpm's normal CI installation behavior and instructs the generator to run the repository's current checks.
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
service-name |
string | No | Repository name | Name of the service that was released |
version |
string | Yes | — | The released version (e.g., 1.2.3) |
release-tag |
string | Yes | — | The raw release tag (e.g., v1.2.3). Used for linking back to the release. |
release-notes |
string | Yes | — | Release notes markdown from the release |
changes-diff |
string | Yes | — | Git diff of changes in this release |
docs-path |
string | Yes | — | Documentation focus hint; existing target-repository rules determine edit scope and page placement |
docs-repo |
string | No | photon-hq/docs |
Target docs repository (owner/repo) |
docs-branch |
string | No | main |
Base branch of the docs repo |
anthropic-api-key |
secret | Yes | — | Anthropic API key for Claude Code |
github-token |
secret | Yes | — | GitHub token with write access to the docs repo |
| Output | Type | Description |
|---|---|---|
pr-url |
string | URL of the created PR (empty if no changes needed) |
has-changes |
string | 'true' or 'false' whether docs updates were generated |
- uses: photon-hq/buildspace/.github/blocks/update-docs@main
with:
version: '1.2.3'
release-tag: 'v1.2.3'
release-notes: 'Added new feature X'
changes-diff: ${{ steps.diff.outputs.diff }}
docs-path: advanced-kits/imessage
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
github-token: ${{ steps.app-token.outputs.token }}Path: .github/blocks/update-skills/action.yaml
Uses Claude Code to analyze release changes and submit a PR to a skills repository. Clones the skills repo, runs the Claude Code CLI to identify and update relevant SKILL.md files, then creates a PR if changes were made. Expects a pre-generated github-token with write access to the skills repo (the update-skills workflow generates this token from the caller-provided App secrets).
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
service-name |
string | No | Repository name | Name of the service that was released |
version |
string | Yes | — | The released version (e.g., 1.2.3) |
release-tag |
string | Yes | — | The raw release tag (e.g., v1.2.3). Used for linking back to the release. |
release-notes |
string | Yes | — | Release notes markdown from the release |
changes-diff |
string | Yes | — | Git diff of changes in this release |
skills-path |
string | Yes | — | Path within the skills repo this service maps to (e.g., skills/buildspace-ci-cd or skills/imessage) |
skills-repo |
string | No | photon-hq/skills |
Target skills repository (owner/repo) |
skills-branch |
string | No | main |
Base branch of the skills repo |
anthropic-api-key |
secret | Yes | — | Anthropic API key for Claude Code |
github-token |
secret | Yes | — | GitHub token with write access to the skills repo |
| Output | Type | Description |
|---|---|---|
pr-url |
string | URL of the created PR (empty if no changes needed) |
has-changes |
string | 'true' or 'false' whether skills updates were generated |
- uses: photon-hq/buildspace/.github/blocks/update-skills@main
with:
version: '1.2.3'
release-tag: 'v1.2.3'
release-notes: 'Added new feature X'
changes-diff: ${{ steps.diff.outputs.diff }}
skills-path: skills/my-service
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
github-token: ${{ steps.app-token.outputs.token }}The primitive catalog documents each action's inputs, outputs, prerequisites, and side effects. These actions do not check out source, install toolchains or dependencies, choose release channels, mint tokens, or approve a release.
| Primitive | One responsibility |
|---|---|
cargo-build |
Compile one Rust binary |
go-build |
Compile one Go binary |
swift-build |
Compile one Swift executable product |
stage-file |
Copy one file to an explicit destination |
pkgbuild |
Build one unsigned macOS installer from a prepared root |
npm-package-info |
Read a package's name and version |
npm-publish |
Run one npm-compatible publication |
wait-npm-version |
Wait for an exact package version to be visible |
cargo-publish |
Run one Cargo publication |
git-stage |
Stage explicit Git pathspecs |
git-commit |
Commit the existing Git index |
git-push |
Push without staging, committing, fetching, or rebasing |
For example, compilation does not require packaging or publishing:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- id: compile
uses: photon-hq/buildspace/.github/primitives/go-build@<buildspace-commit-sha>
with:
output: bin/worker
goos: linux
goarch: amd64
- id: artifact
uses: photon-hq/buildspace/.github/primitives/stage-file@<buildspace-commit-sha>
with:
source: ${{ steps.compile.outputs.binary-path }}
destination: artifacts/worker-linux-amd64
executable: 'true'Replace both placeholders with the same full Buildspace commit SHA. Uploading the file is a separate step, using steps.artifact.outputs.path with actions/upload-artifact if needed.
The refactored build, publication, and Git mutation recipes use GitHub's $/ self-repository references. Nested actions resolve to the recipe's own commit, not the caller's checkout or a floating @main. These references require GitHub.com; GitHub Enterprise Server does not support them. Native primitives use the runner's Node 24 action runtime and do not change the caller's Node version.
The stage/promote workflows still enforce their existing provenance, approval, and credential boundaries. Raw publication primitives are not a replacement for those checks. Release gates, environments, and credential scope belong to the composing workflow.
buildspace/
├── .github/
│ ├── blocks/ # Composite actions (building blocks)
│ │ ├── bump-monorepo-versions/ # AI version bump for monorepos
│ │ ├── check-pr-label/ # Generic inspection; retained for compatibility callers
│ │ ├── check-readme/ # AI README freshness check
│ │ ├── comment-on-pr/ # Post/update PR comments
│ │ ├── create-github-release/ # GitHub Release creation
│ │ ├── detect-changed-packages/ # Monorepo change detection + topo-sort
│ │ ├── determine-publish-version/ # AI version detection (standalone)
│ │ ├── generate-release-info/ # AI version + release notes
│ │ ├── go-build/ # Go cross-compilation
│ │ ├── publish-crates/ # crates.io publishing
│ │ ├── publish-github-package/ # GitHub Packages publishing (single package)
│ │ ├── publish-github-packages/ # GitHub Packages publishing (monorepo, ordered)
│ │ ├── publish-npm/ # npm publishing (single package)
│ │ ├── publish-npm-packages/ # npm publishing (monorepo, ordered)
│ │ ├── rust-build/ # Cross-platform Rust builds
│ │ ├── swift-build/ # Swift binary builds
│ │ ├── swift-pkg/ # macOS .pkg creation
│ │ ├── sync-crates-version/ # Workspace version sync
│ │ ├── typescript-build/ # TypeScript builds
│ │ ├── update-docs/ # AI docs update via Claude Code
│ │ └── update-skills/ # AI skills update via Claude Code
│ │
│ ├── primitives/ # Single-operation actions, shared by recipes
│ │ ├── cargo-build/ # Compile a Rust binary
│ │ ├── go-build/ # Compile a Go binary
│ │ ├── swift-build/ # Compile a Swift product
│ │ ├── stage-file/ # Copy one artifact
│ │ ├── pkgbuild/ # Package a prepared macOS install root
│ │ ├── npm-package-info/ # Read package identity
│ │ ├── npm-publish/ # Publish once, without auth fallback policy
│ │ ├── wait-npm-version/ # Read exact registry visibility
│ │ ├── cargo-publish/ # Publish one crate
│ │ ├── git-stage/ # Stage explicit pathspecs
│ │ ├── git-commit/ # Commit an existing index
│ │ └── git-push/ # Push with caller-selected Git policy
│ │
│ ├── package-release/ # Stage/promote versioning, manifests, and publishers
│ └── workflows/ # Reusable workflows (full pipelines)
│ ├── check-readme.yaml # AI README freshness check (PR)
│ ├── package-stage.yml # Stage packages and their production candidates
│ ├── package-promote.yml # Promote stored package candidates after approval
│ ├── swift-stage.yml # Stage Swift packages or libraries
│ ├── swift-promote.yml # Promote Swift candidates; optional Jamf delivery
│ ├── go-service-release.yaml # Compatibility: direct Go releases
│ ├── rust-service-release.yaml # Compatibility: direct Rust releases
│ ├── pkg-release.yml # Compatibility: payload-only .pkg release
│ ├── pkg-release-pr.yml # .pkg PR build (no binary)
│ ├── swift-pkg-pr.yml # Swift .pkg build on every PR commit
│ ├── swift-release.yml # Compatibility: direct Swift .pkg releases
│ ├── typescript-monorepo-release.yaml # Compatibility: independent package releases
│ ├── typescript-service-release.yaml # Compatibility: direct TS releases
│ ├── update-docs.yaml # AI docs update on release
│ └── update-skills.yaml # AI skills update on release
- Create a feature branch from
main - Make your changes
- Test locally or in a test repository
- Open a PR with a clear description
- Merge approved changes to
main; BuildSpace itself requires no release label
Run the release-workflow tests from the repository root:
npm ci --prefix .github/blocks/check-production-dependencies --ignore-scripts --no-audit --no-fund
npm ci --prefix .github/package-release --ignore-scripts --no-audit --no-fund
node --test test/*.test.mjs .github/blocks/check-production-dependencies/*.test.mjs
python3 -m unittest discover -s testPython tests require PyYAML 6.0.3. To test a package or Swift staging workflow
without publishing, use its dry-run: true mode:
with:
dry-run: truePoint the staging call to your branch while testing:
uses: photon-hq/buildspace/.github/workflows/package-stage.yml@your-branchLegacy publisher dry-run flags do not prevent every write: version commits
and GitHub Releases can still occur. Do not use them as a substitute for a
modern staging dry run.
MIT © Photon