Skip to content

About

Reusable GitHub Actions blocks and workflows for fast shipping teams

Topics

Resources

Stars

15 stars

Watchers

0 watching

Forks

Latest commit

 

History

280 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BuildSpace

GitHub Actions Rust TypeScript NPM Crates.io

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.

Table of Contents


Quick Start

Which workflow do I need?

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)

Stage first, then promote

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.

Existing direct-release callers

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.

README Update-To-Date Check

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.

Pinning BuildSpace

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.


Prerequisites

Required Secrets

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.

Release policy

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.

Permissions

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.


Workflows

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.


Immutable Image Publisher

.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-* and hotfix-* tags. Photon's existing Terraform uses IMMUTABLE_WITH_EXCLUSION with only the exact floating main tag excluded; never widen that exception to main* or hotfix-*. 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-args adds non-secret build arguments. GIT_SHA is always the event commit and cannot be replaced.
  • submodules is 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-repositories and pass app-id and app-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 to prepare or to the build.
  • prepare runs 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.created set 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:BatchGetImage as well as ecr: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-workflow permits 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-repositories and pass app-id and app-private-key. The workflow mints a GitHub App token limited to reading those repositories and mounts it as the BuildKit secret named by app-token-secret (default github_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).


Package Stage and Promote

.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 pack script is the caller's. It runs twice, with RELEASE_CHANNEL (staging or production), RELEASE_VERSION (the build's X.Y.Z), RELEASE_SUFFIX (-staging.<run id>.<attempt>, or empty) and PACK_DESTINATION. It must write one .tgz per package to PACK_DESTINATION, each versioned $RELEASE_VERSION$RELEASE_SUFFIX, and test what it packed. It must leave tracked files unchanged; the job fails otherwise. verify runs once before both packs. Both run with NODE_AUTH_TOKEN able 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_TOKEN secret and use it in install, for example install: pnpm install --frozen-lockfile --config.//registry.example/:_authToken="$INSTALL_TOKEN". Only the install script sees it; verify and pack do not.
  • npm-only repositories. Set setup-pnpm: false and install: npm ci. Node supplies npm; the workflow skips pnpm's package-manager-field check. Existing callers still set up their pinned pnpm by default, and bun-version continues 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 after package (default @<owner>/<repository>), which must be one of them. tag-prefix overrides 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-prefix and artifact-name, and their own promote workflow. A workflow that promotes several of them in one run, in dependency order, gives each promote call its own artifact-name as well. Give each its own paths too, 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.Z tags already name something else, such as a service that has its own releases, sets production-tag-prefix to <name>-v in both workflows. Its packages are then released as <name>-vX.Y.Z, and promotion neither needs nor touches the vX.Y.Z tags. Use the same value in the stage and the promote caller: a build staged under one prefix is refused under another. Crates always use vX.Y.Z, so a build with crates cannot 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 environment input (default staging); restrict that environment to main. Promotion requires main, verifies each candidate's checksum and its attestation (signer photon-hq/buildspace/.github/workflows/package-stage.yml, the caller repository, refs/heads/main and the source commit), and refuses unless the production environment exists with required reviewers or the run is the caller's trusted-actor dispatching (below).
  • Promoting the build of a service image. A repository that also ships an image passes image-tag (main-<sha>) in place of staging-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. A hotfix-<baseline>-<sha> tag releases nothing: packages are staged from main.
  • 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-actor names an actor whose own workflow_dispatch is 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.org and package, its scoped name such as @acme/app, in both callers, and grants id-token: write to 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 example publish.yml with environment staging and promote.yml with environment production, and allow both npm publish and npm dist-tag on each. Leave the environment off the promote publisher if a trusted-actor promotes, 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-version in the stage caller, which sets up Bun in place of pnpm, and its own install, such as bun install --frozen-lockfile.
  • Promotion refuses a candidate that is not newer than latest or is already published with other contents, and one whose same-scope dependencies or optionalDependencies are 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 crates are 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:
    acme-error = { version = "=0.1.0", git = "https://github.com/acme/error", tag = "v0.3.0" }
    Pin the staging tag to try a staging build. After verify, the build job runs cargo package --locked --no-verify --exclude-lockfile for each crate (Cargo 1.87+, clean checkout); the .crate records 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 in Cargo.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 example 0.4.0 after 0.3.2 for 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 a vX.Y.Z tag with an exact =X.Y.Z version. 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.json versions are not read. It starts from the highest stable <production-tag-prefix>X.Y.Z tag and reads every first-parent commit on main since then that touches paths (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 a BREAKING CHANGE: footer is major, feat minor, and anything else a patch, including a title that does not parse, which the run warns about. Below 1.0 a breaking change releases 1.0.0.
    • Merges between promotions do not add up: from v1.26.0, a fix stages 1.26.1, a later feat 1.27.0, a later docs still 1.27.0, and promoting that build makes v1.27.0 the next starting point.
    • A Release-As: X.Y.Z line in a description raises the version to at least X.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 at 0.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 paths counted, the staging run warns that it cannot be promoted: widen paths or merge a Release-As.
  • Moving to computed versions. Builds staged before it keep the version their package.json gave them and still promote. A caller moving its pins grants pull-requests: read to the stage call, passes the paths its push trigger filters on as paths, and packs at $RELEASE_VERSION instead of the package.json version. Where package.json is ahead of the latest release with an unpromoted bump, the moving pull request's description carries Release-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-workflow waits up to ci-wait-minutes (default 20) for that workflow's push run on the exact commit to succeed. dry-run: true verifies and packs without publishing and is the only mode allowed outside main, so a pull request branch can dispatch it.
  • downstream repositories receive an internal-package-published dispatch after each publication, using the APP_ID and APP_PRIVATE_KEY GitHub 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).


Production dependency check

Action: .github/blocks/check-production-dependencies

  • Shared by Kargo source-repository preflights and manual checks.
  • Pass image-tag (main-<sha> or hotfix-<baseline>-<sha>) or source-ref.
  • Audits what that commit has at its root: a pnpm workspace (pnpm-workspace.yaml), a Cargo workspace (Cargo.toml with its committed Cargo.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's peerDependencies may 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 local workspace: 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.Z tag (see Package Stage and Promote). Every such crate in Cargo.lock, transitive ones included, must resolve from a vX.Y.Z tag at an exact stable version; a rev, 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 commit Cargo.lock resolved. Each workspace package's own declarations (normal, dev, build and target dependencies, inherited ones included) must pin that tag with an exact =X.Y.Z version, 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: read and packages: 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 with contents: read in 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+).


Rust Service Release

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.

Inputs

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

Secrets

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)

Example

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 }}

TypeScript Service Release

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.

Inputs

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

Secrets

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

Example

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, add id-token: write to the caller's permissions, configure a trusted publisher for the package on npmjs.com, and ensure the runner has npm ≥ 11.5.1. Callers that don't set use-oidc are completely unaffected — they keep contents: read least-privilege and publish via NPM_TOKEN exactly 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: true and add packages: write to the caller's permissions. The package's name must be scoped to the repository owner (for example, @acme/app). BuildSpace authenticates with the automatic GITHUB_TOKEN, so no PAT or additional secret is required. Leave no-npm-publish as false to publish to both registries, or set it to true for GitHub Packages only.


TypeScript Monorepo Release

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.

Inputs

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

Secrets

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)

Example

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.

How It Works

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  │
                                     └──────────────┘  └─────────────┘

Go Binary Release

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.

Inputs

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

Secrets

Secret Required Description
OPENAI_API_KEY Yes For AI-powered versioning and release notes

Example

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 }}

Swift Stage and Promote

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

  1. A read-only job computes X.Y.Z from the pull requests merged since the latest vX.Y.Z.
  2. 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 at X.Y.Z. Each compiles with a .env of VERSION, BUILD_ENVIRONMENT=production, RELEASE_CHANNEL (staging or production) and SECRET_ENV_VARS, and pkgbuild packages it at its version.
  3. 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.candidate and release-manifest.json. GitGate and the Astrolabe self-updater install the first .pkg of 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, entitlements and private-deps (with the APP_ID and APP_PRIVATE_KEY secrets) work as in Swift Release. paths, ci-workflow, ci-wait-minutes, environment (default staging) and dry-run work as in package-stage. The promote call names the same package-name and library, and sets private-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-version names, or the latest Swift release when it has none. swiftly rewrites .swift-version when 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.resolved and Package.swift from 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 committed Package.resolved, which the build resolved from and could not change: each must be a stable X.Y.Z (not a branch, a bare revision or a staging version) whose vX.Y.Z or X.Y.Z tag is a published, non-prerelease release still naming the pinned commit. A library, whose consumers resolve its requirements themselves, and a package that commits no Package.resolved are held to the requirements in Package.swift instead: 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 takes private-deps. The reviewer's summary lists each release it checked: the pinned version from Package.resolved, or the lowest version Package.swift requires, which SwiftPM may resolve higher.
  • Libraries. library: true builds nothing. Stage waits for CI, then tags vX.Y.Z-staging.<run id>.<attempt> with a prerelease that holds only the manifest, and promotion tags vX.Y.Z on the same commit. SwiftPM resolves only stable versions for a requirement such as from: "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> as VERSION and the candidate X.Y.Z. Both keep BUILD_ENVIRONMENT=production, as release builds always have, so staging Macs' telemetry keeps its environment; RELEASE_CHANNEL tells 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 to vX.Y.Z until 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 (signer photon-hq/buildspace/.github/workflows/swift-stage.yml, the caller repository, refs/heads/main and the source commit), whose version is not newer than the latest vX.Y.Z, or that depends on the owner's repositories other than through production releases (see Dependencies), and it refuses unless the production environment requires a reviewer. A build already released as vX.Y.Z from 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_VARS and 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. The release and prerelease labels and OPENAI_API_KEY are no longer used. The first staged version follows the latest stable vX.Y.Z; -rc.N tags are ignored, and a repository without a stable tag starts at 0.1.0 unless the moving pull request carries Release-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).


Swift Release

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.

Inputs

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

Secrets

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

Example

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 }}

Package Release

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.

Inputs

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

Secrets

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

Example

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 }}

Package PR Build

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.

Inputs

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

Example

# .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: scripts

Swift Package PR Build

File: .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.

  1. Posts a "Building..." comment on the PR (or updates the existing one)
  2. Builds the Swift binary, creates a .pkg versioned as pr.<PR#>.<run#>
  3. Uploads the .pkg as an Actions artifact (7-day retention)
  4. Updates the PR comment to success (with artifact link) or failure (with log link)

Inputs

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

Secrets

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

Example

# .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 }}

Check README

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.

Inputs

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.

Secrets

Secret Required Description
OPENAI_API_KEY Yes For AI-powered README analysis

Example (warning only)

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 }}

Example (block the PR)

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-readme as a required status check in your branch protection rules (Settings > Branches > Branch protection rule).


Update Documentation

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.

Inputs

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

Secrets

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.

Example

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 }}

Update Skills

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.

Inputs

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

Secrets

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.

Example

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 }}

How Releases Work

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]
Loading

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.


Blocks (Composite Actions)

Individual building blocks that the workflows above are assembled from. Use these directly when you need a custom pipeline.

Expand all blocks

collect-release-context

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 }}

check-pr-label

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.

Inputs

Input Type Required Default Description
labels string Yes — JSON array of labels to check (e.g., ["needs-review", "documentation"])

Outputs

Output Type Description
labels string JSON-encoded object with lowercase keys and boolean values (e.g., {"needs-review": true, "documentation": false})

Usage

- 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"

check-readme

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.

Inputs

Input Type Required Default Description
openai-api-key secret Yes — OpenAI API key

Outputs

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

Usage

- 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 }}"

generate-release-info

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.

Inputs

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

Outputs

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

Usage

- 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 }}"

determine-publish-version

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.

Inputs

Input Type Required Default Description
prerelease boolean No false Append -rc.N suffix to version
openai-api-key secret Yes — OpenAI API key

Outputs

Output Type Description
version string Determined version (e.g., 1.2.3)
previous-version string Previous version before this release

Usage

- 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 }}"

create-github-release

Path: .github/blocks/create-github-release/action.yaml

Creates a GitHub Release with optional artifact attachments.

Inputs

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

Outputs

Output Type Description
url string URL of the created release
tag string Created tag name (e.g., v1.2.3)

Usage

- 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-*"

detect-changed-packages

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.

Inputs

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

Outputs

Output Type Description
changed JSON Array of changed packages in topological order
has-changes boolean Whether any packages have changes

Usage

- 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 }}"

bump-npm-version

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

Inputs

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

Usage

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


bump-monorepo-versions

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.

Inputs

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

Outputs

Output Type Description
versions JSON Object mapping package names to new versions
release-notes string Combined AI-generated release notes

Usage

- 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 }}

publish-github-packages

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.

Inputs

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

Usage

- 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 }}

publish-npm-packages

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.

Inputs

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

Usage

- 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 }}

rust-build

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)

Inputs

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

Outputs

Output Type Description
artifact-name string Name of the built artifact
artifact-path string Path to the artifact file

Usage

- uses: photon-hq/buildspace/.github/blocks/rust-build@main
  with:
    binary-name: my-cli
    target: x86_64-unknown-linux-gnu

typescript-build

Path: .github/blocks/typescript-build/action.yaml

Builds a TypeScript project using Bun.

Inputs

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

Usage

- uses: photon-hq/buildspace/.github/blocks/typescript-build@main
  with:
    build-command: "npm run build"

sync-crates-version

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.

Inputs

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

Outputs

Output Type Description
updated-crates JSON Array of crate names that were updated

Usage

- uses: photon-hq/buildspace/.github/blocks/sync-crates-version@main
  with:
    version: "1.2.3"
    github-token: ${{ github.token }}

update-tap

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.

Inputs

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

publish-crates

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.

Inputs

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

Usage

- uses: photon-hq/buildspace/.github/blocks/publish-crates@main
  with:
    crates: '["crates/shared", "crates/client"]'
    cargo-registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}

publish-npm

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.

Inputs

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

Usage

- uses: photon-hq/buildspace/.github/blocks/publish-npm@main
  with:
    build-command: "npm run build"
    npm-token: ${{ secrets.NPM_TOKEN }}

Publishing modes

  1. 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.
  2. Token fallback: if OIDC is unavailable (no id-token: write) or fails, the action publishes with npm-token. This is the default behavior today — keep providing NPM_TOKEN so publishing works until OIDC is fully set up.

publish-github-package

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.

Inputs

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

Usage

- uses: photon-hq/buildspace/.github/blocks/publish-github-package@main
  with:
    build-command: "npm run build"
    github-token: ${{ github.token }}

comment-on-pr

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.

Inputs

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

Usage

- 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 }}

update-docs

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.

Inputs

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

Outputs

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

Usage

- 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 }}

update-skills

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

Inputs

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

Outputs

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

Usage

- 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 }}

Primitives

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.

Architecture

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

Contributing

  1. Create a feature branch from main
  2. Make your changes
  3. Test locally or in a test repository
  4. Open a PR with a clear description
  5. 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 test

Python 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: true

Point the staging call to your branch while testing:

uses: photon-hq/buildspace/.github/workflows/package-stage.yml@your-branch

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


License

MIT © Photon

About

Reusable GitHub Actions blocks and workflows for fast shipping teams

Topics

Resources

Stars

15 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages