Reusable, function-named CI/CD building blocks for Nurdsoft projects.
Each action is named for the pipeline function it performs, not a language
or tool — so the implementation underneath is pluggable while the public
interface stays stable. The repo ships three concerns and keeps only the
generic two: it provisions (auth, toolchain install) and orchestrates
(artifacts, PR comments, notifications); the project-specific commands live
behind a runner contract you control (or pass inline via run).
| Path | Type | Function |
|---|---|---|
.github/workflows/version.yml |
Reusable workflow | Cut a SemVer release (stable or RC prerelease) |
actions/auth |
Action | Obtain cloud credentials (OIDC) |
actions/setup |
Action | Install runtime + deps (+ EAS login) |
actions/verify |
Action | Lint / type-check / test |
actions/build |
Action | Produce a deployable artifact — static or Docker image |
actions/deploy |
Action | Ship the artifact — static site, Cloud Run service, or Cloud Run job (+ optional Cloud Scheduler) |
actions/plan |
Action | Preview an infrastructure change |
actions/apply |
Action | Apply an infrastructure change |
actions/notify |
Action | Post the pipeline result |
- Function-named directories — swap Node→Bun or GCP→AWS by changing inputs, not the directory.
- No forced Makefile — phase actions take a
runcommand (or arunner);makeis only the default our repos opt into (see Runner contract). - Provision vs execute vs orchestrate — credentials and tools are installed here; the commands that use them live in the consumer.
- Reusable workflow for release —
version.ymlis a workflow (not an action) because cutting a release needs its owncontents: writejob.
Callers wire the actions into a job graph and supply their own values. Two illustrative shapes:
App pipeline — static site (verify, release, build, deploy)
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: nurdsoft/ci-workflows/actions/verify@v2
version:
needs: [verify]
uses: nurdsoft/ci-workflows/.github/workflows/version.yml@v2
permissions: { contents: write }
with:
rc-line: "1-rc" # rc off non-default branches; stable on default
build:
needs: [version]
runs-on: ubuntu-latest
steps:
- uses: nurdsoft/ci-workflows/actions/build@v2
with:
run: <your build command> # or rely on `make build`
output: artifact
deploy:
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: nurdsoft/ci-workflows/actions/deploy@v2
with:
run: <your deploy command> # or rely on `make deploy`
download-artifact: "true"
gcp-wif-provider: ${{ secrets.WIF_PROVIDER }}
gcp-service-account: ${{ secrets.SERVICE_ACCOUNT }}App pipeline — Docker + Cloud Run (release, build, deploy)
Activated by passing image-name to build and cloudrun-service to deploy. The static-build and static-deploy steps are skipped automatically — existing callers are unaffected.
jobs:
version:
uses: nurdsoft/ci-workflows/.github/workflows/version.yml@v2
permissions: { contents: write }
with:
rc-line: "1-rc" # rc off non-default branches; stable on default
build:
needs: [version]
runs-on: ubuntu-latest
environment: dev
steps:
- uses: nurdsoft/ci-workflows/actions/build@v2
with:
gcp-wif-provider: ${{ secrets.GCP_WIF_PROVIDER }}
gcp-service-account: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }}
gcp-project-id: ${{ secrets.GCP_PROJECT_ID }}
gcp-region: ${{ secrets.GCP_REGION }}
gcp-repository: ${{ secrets.GCP_REPOSITORY }}
image-name: ${{ secrets.IMAGE_NAME }}
gcp-secret-name: ${{ secrets.GCP_SECRET_NAME }} # fetched → .env.production at build time
deploy:
needs: [build]
runs-on: ubuntu-latest
environment: dev
steps:
- uses: nurdsoft/ci-workflows/actions/deploy@v2
with:
gcp-wif-provider: ${{ secrets.GCP_WIF_PROVIDER }}
gcp-service-account: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }}
gcp-project-id: ${{ secrets.GCP_PROJECT_ID }}
gcp-region: ${{ secrets.GCP_REGION }}
gcp-repository: ${{ secrets.GCP_REPOSITORY }}
image-name: ${{ secrets.IMAGE_NAME }}
cloudrun-service: ${{ secrets.CLOUDRUN_SERVICE_NAME }}
gcp-secret-name: ${{ secrets.GCP_SECRET_NAME }} # fetched → injected as Cloud Run env vars
cloudrun-flags: "--allow-unauthenticated --ingress=internal-and-cloud-load-balancing"App pipeline — GHCR pull + retag + Cloud Run (pre-built image)
For services whose Docker image is built and published to GHCR by a separate process (e.g. a commerce platform). Activated by passing ghcr-image to build alongside image-name. The action pulls the pre-built image, re-tags it for GCP Artifact Registry (SHA + latest), and pushes it — no Dockerfile or build-time secrets required. Takes priority over the Docker build+push path.
jobs:
version:
uses: nurdsoft/ci-workflows/.github/workflows/version.yml@v2
permissions: { contents: write }
with:
rc-line: "1-rc"
build:
needs: [version]
runs-on: ubuntu-latest
environment: dev
steps:
- uses: nurdsoft/ci-workflows/actions/build@v2
with:
gcp-wif-provider: ${{ secrets.GCP_WIF_PROVIDER }}
gcp-service-account: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }}
gcp-project-id: ${{ secrets.GCP_PROJECT_ID }}
gcp-region: ${{ secrets.GCP_REGION }}
gcp-repository: ${{ secrets.GCP_REGISTRY }}
image-name: ${{ vars.SERVICE_NAME }}
ghcr-image: ghcr.io/org/repo:latest # source image; triggers pull+retag path
deploy:
needs: [build]
runs-on: ubuntu-latest
environment: dev
steps:
- uses: nurdsoft/ci-workflows/actions/deploy@v2
with:
gcp-wif-provider: ${{ secrets.GCP_WIF_PROVIDER }}
gcp-service-account: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }}
gcp-project-id: ${{ secrets.GCP_PROJECT_ID }}
gcp-region: ${{ secrets.GCP_REGION }}
gcp-repository: ${{ secrets.GCP_REGISTRY }}
image-name: ${{ vars.SERVICE_NAME }}
cloudrun-service: ${{ vars.SERVICE_NAME }}
gcp-secret-name: ${{ secrets.GCP_SECRET_NAME }}
cloudrun-flags: >-
--vpc-connector="${{ secrets.GCP_VPC_CONNECTOR }}"
--ingress=internal-and-cloud-load-balancingApp pipeline — Docker + Cloud Run job with Cloud Scheduler
Activated by passing cloudrun-job to deploy instead of cloudrun-service. Optionally reconciles a Cloud Scheduler trigger (created if missing, updated if existing) when scheduler-name and schedule-time are set.
jobs:
version:
uses: nurdsoft/ci-workflows/.github/workflows/version.yml@v2
permissions: { contents: write }
with:
rc-line: "1-rc"
build:
needs: [version]
runs-on: ubuntu-latest
environment: dev
steps:
- uses: nurdsoft/ci-workflows/actions/build@v2
with:
gcp-wif-provider: ${{ secrets.GCP_WIF_PROVIDER }}
gcp-service-account: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }}
gcp-project-id: ${{ secrets.GCP_PROJECT_ID }}
gcp-region: ${{ secrets.GCP_REGION }}
gcp-repository: ${{ secrets.GCP_REPOSITORY }}
image-name: ${{ secrets.IMAGE_NAME }}
gcp-secret-name: ${{ secrets.GCP_SECRET_NAME }}
deploy-job:
needs: [build]
runs-on: ubuntu-latest
environment: dev
steps:
- uses: nurdsoft/ci-workflows/actions/deploy@v2
with:
gcp-wif-provider: ${{ secrets.GCP_WIF_PROVIDER }}
gcp-service-account: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }}
gcp-project-id: ${{ secrets.GCP_PROJECT_ID }}
gcp-region: ${{ secrets.GCP_REGION }}
gcp-repository: ${{ secrets.GCP_REPOSITORY }}
image-name: ${{ secrets.IMAGE_NAME }}
cloudrun-job: ${{ secrets.CLOUDRUN_JOB_NAME }}
gcp-secret-name: ${{ secrets.GCP_SECRET_NAME }}
cloudrun-flags: >-
--command="/app/server"
--args="worker"
--vpc-connector=${{ secrets.VPC_CONNECTOR }}
scheduler-name: my-job-scheduler-trigger # omit to skip scheduler management
schedule-time: "0 * * * *" # cron expression — hourlyInfrastructure pipeline — plan then apply the same plan
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: nurdsoft/ci-workflows/actions/plan@v2
with:
env: <env>
aws-role-arn: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ secrets.AWS_REGION }}
github-token: ${{ secrets.GITHUB_TOKEN }}
apply:
needs: [plan]
runs-on: ubuntu-latest
steps:
- uses: nurdsoft/ci-workflows/actions/apply@v2
with:
env: <env>
aws-role-arn: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ secrets.AWS_REGION }}Phase actions (build, deploy, plan, apply) run a command — supply it any
of three ways: pass run/run-* directly (no Makefile), set runner to your
tool (just, task, npm run), or implement the default make targets.
| Phase | Default command(s) | Env provided |
|---|---|---|
| build | make build |
ENV, APP_VERSION* |
| deploy | make deploy |
ENV |
| plan | make tf-init, make tf-plan (+ tf-fmt/tf-validate for terraform) |
ENV, TARGET |
| apply | make tf-init, make tf-apply |
ENV, TARGET |
* build exports APP_VERSION under the name given by version-env-var.
Self-contained — no contract, no Makefile: auth, setup, verify, notify,
and the version.yml reusable workflow.
Docker / Cloud Run path: when
image-name(build) orcloudrun-service/cloudrun-job(deploy) is set, the runner contract is bypassed entirely — the action handles auth, build, and deploy against GCP Artifact Registry and Cloud Run directly. No Makefile targets required.Cloud Run job path: when
cloudrun-job(deploy) is set instead ofcloudrun-service, the action deploys a Cloud Run job and optionally reconciles a Cloud Scheduler trigger (created if missing, updated if existing). Passscheduler-nameandschedule-timeto enable scheduling; omit both to skip it.GHCR pull + retag path: when
ghcr-image(build) is also set, the action pulls the pre-built image from GHCR and re-tags it for Artifact Registry instead of building from source. No Dockerfile or build-time secrets needed.
Pin to the major tag (@v2). Breaking changes ship under a new major; the
previous major stays in place for un-migrated callers. Third-party actions are
SHA-pinned and bumped by Dependabot.
See CONTRIBUTING.md. PRs are linted with actionlint (and
shellcheck over composite run: blocks) via .github/workflows/ci.yml.