Shared CI/CD workflows and composite actions for Polygon repositories. The contents fall into two families:
- Apps Team reusable workflows + composite actions — prefixed
apps-*.ymlin.github/workflows/and living under.github/actions/. Migrated here from0xPolygon/apps-team-workflowsso that public-visibility consuming repos can call them directly (GitHub forbids private → public workflow calls). - Shared org infrastructure workflows — image release, versioning, security scanning, and legacy AWS/ECS deploys used by services across the orgs.
All workflows here are on: workflow_call. Consumers supply the event
trigger via a thin trigger file in their own repo.
Called via
uses: 0xPolygon/pipelines/.github/workflows/<name>.yml@main (or via a
local copy for public repos that prefer vendored workflows). Each runs
in its own separate GitHub Actions job with its own runner.
| Workflow | Purpose | Required secrets |
|---|---|---|
apps-ci.yml |
Lint, typecheck, and test on PRs | (none) |
apps-changeset-check.yml |
PR gate: requires a changeset; posts/deletes instructions comment | (none) |
apps-codegen-drift-check.yml |
PR gate: runs pnpm -r --include-workspace-root run --if-present codegen-drift-check (no-op when no package defines the script) |
(none) |
apps-npm-release.yml |
Changesets release pipeline (version bumps, npm publish, git tags) and on-demand snapshot publish via the snapshot_tag input |
CHANGESET_RELEASE_BOT_APP_ID, CHANGESET_RELEASE_BOT_APP_PRIVATE_KEY |
apps-docker-release.yml |
GCP image push on version tag (delegates to gcp_pipeline_release_image.yaml) |
build_params_gh_secret_keys |
apps-pr-labeler.yml |
Labels .github/-only PRs as do-not-notify; removes label when non-.github/ changes are added |
(none) |
apps-slack-merge-notify.yml |
Posts a Slack Block Kit message when a PR is merged | SLACK_WEBHOOK_URL |
apps-claude-code-review.yml |
Automated Claude Code PR review | CLAUDE_API_KEY |
apps-claude.yml |
Interactive Claude Code agent (triggered by @claude mentions) | CLAUDE_API_KEY |
Trigger file examples for each of the above live alongside them as
apps-*-trigger.yml and also serve as canonical templates for consuming
repos.
Called via uses: 0xPolygon/pipelines/.github/actions/<name>@main as a
step inside a job. Composite actions run in the calling job's
environment — they inherit the job's env: block, runners, and file
system state automatically.
| Action | Purpose | Has compiled dist? |
|---|---|---|
actions/ci |
Checkout, install, lint, typecheck, test | No — pure shell |
actions/check-dockerfile |
Pre-flight gate: resolve a changeset tag and report whether the package ships a Dockerfile | No — pure shell |
actions/docker-test |
Resolve service from tag, build image, start container, run tests, stop | No — pure shell |
actions/npm-release |
Publish, tag, and release post-changesets merge | No — bash script |
actions/slack-notify |
Post Slack Block Kit message from pr.json |
Yes — ncc bundle |
actions/upsert-changeset-comment |
Post or remove changeset nag comment on a PR | Yes — ncc bundle |
Runs pnpm run lint, pnpm run --if-present typecheck, and pnpm run --if-present test
in the calling job. Because it runs as a step (not a separate runner), any env vars
composed from secrets in the trigger's job.env: block are automatically available
to pnpm test — no secret-passing mechanism needed:
jobs:
ci:
name: CI - lint / typecheck / test
runs-on: ubuntu-latest
env:
MY_RPC: https://rpc.example.com?token=${{ secrets.MY_TOKEN }}
steps:
- uses: 0xPolygon/pipelines/.github/actions/ci@mainRepositories with no test env var requirements omit the env: block entirely.
A discrete PR gate for "edited the schema but forgot to regenerate" bugs.
Runs pnpm -r --include-workspace-root run --if-present codegen-drift-check
across the workspace in topological order. Two patterns both work:
- Root-only — a repo with no workspace children (e.g.
apps-team-ops, wherepnpm-workspace.yamlhas nopackages:field) defines the script at the root.--include-workspace-rootis required so plainpnpm -rdoesn't skip it. - Per-package — a monorepo where each package owns its own check
(e.g. an orval client that regenerates from a schemas package's
openapi.json). Topological order means schemas regenerate before downstream clients — drift produced by one step is visible to the next.
The codegen-drift-check convention: regenerate the artifacts in place
and exit non-zero when git diff reports any change.
A separate workflow rather than another step in actions/ci, so the
failure surfaces under its own PR check name — easy to make a required
status on main and trivial to triage at a glance. The --if-present
keeps it a fast green no-op for repos that ship no codegen, so the
canonical CI trigger can carry the job even before every consumer has
something to check.
The canonical apps-ci-trigger.yml already wires this in as a second
job alongside lint-typecheck-test — adopting team-wide is a single
trigger file in each consumer repo, not two.
Resolves a changeset tag to a workspace package directory and reports whether
that package contains a Dockerfile. Triggers that pay for expensive setup
(Firestore emulator, OIDC auth, fixture seeding) before invoking docker-test
use this as a pre-flight to short-circuit library-only release tags
(spol-contracts-sdk@…, spol-db@…) — the trigger gates every
Dockerfile-dependent step on steps.gate.outputs.has_dockerfile == 'true'.
docker-test already returns should_build=false for the same library case,
but it does so after actions/checkout, pnpm install, and the buildx setup
have run. For triggers with no expensive pre-test steps (e.g. the
example-rest-api template) this action is unnecessary; reach for it only
when the trigger has setup that's wasted on a library tag.
Self-contained — does its own actions/checkout and pnpm/action-setup,
matching the docker-test convention. Discovers the package directory via
pnpm --filter ls --json, so it works regardless of workspace layout
(packages/*, apps/*, nested, root-level — anything pnpm-workspace.yaml
declares). Outputs has_dockerfile, service, and package_dir.
jobs:
test:
runs-on: ubuntu-latest
env:
FIRESTORE_EMULATOR_HOST: 172.17.0.1:8080
# ... other env vars
steps:
- id: gate
uses: 0xPolygon/pipelines/.github/actions/check-dockerfile@main
with:
tag: ${{ inputs.tag || '' }}
- if: steps.gate.outputs.has_dockerfile == 'true'
run: docker compose -f ./docker-compose.test.yml up -d --wait
- id: docker-test
if: steps.gate.outputs.has_dockerfile == 'true'
uses: 0xPolygon/pipelines/.github/actions/docker-test@main
with:
tag: ${{ inputs.tag || '' }}
test_vars: FIRESTORE_EMULATOR_HOST # ...Resolves a deployable service from a changeset git tag, builds its Docker image,
starts the container, waits for /health-check, runs the test suite via
TEST_BASE_URL, then stops the container. Outputs the resolved image_name,
image_tag, dockerfile_path, checkout_ref, and should_build for use by the
subsequent release job. Packages without a Dockerfile in their package directory
exit cleanly with should_build=false.
jobs:
test:
runs-on: ubuntu-latest
outputs:
image_name: ${{ steps.docker-test.outputs.image_name }}
# ... other outputs
steps:
- id: docker-test
uses: 0xPolygon/pipelines/.github/actions/docker-test@main
with:
tag: ${{ inputs.tag || '' }}
# test_vars: MY_RPC # space-separated env var names to forward to containerUsed internally by apps-slack-merge-notify.yml and apps-changeset-check.yml
respectively. Both contain logic that runs across repository boundaries inside a
reusable workflow context, so they must be compiled ncc bundles (raw
.github/scripts/ files are not accessible in the calling repo's workspace).
See Adding a new composite action with compiled dist.
| Workflow | Purpose |
|---|---|
gcp_pipeline_release_image.yaml |
Canonical Docker image build + push to GCP Artifact Registry with OIDC auth. The Apps Team apps-docker-release.yml delegates to this. |
generate_version.yaml |
Produces a deterministic version string <iso-date>-<short-sha>-<run-id>-<run-number> for consumers that need a build identifier. |
codeql.yml |
GitHub CodeQL security scanning. Generic template — consumers customise language matrix. |
security-build.yml |
SonarCloud scan (SONAR_TOKEN) on push to main/dev/staging and on PRs. |
Kept for consumers that have not yet migrated off AWS ECS. New services should use the GCP pipeline instead.
| Workflow | Purpose |
|---|---|
ecs_deploy_docker_taskdef.yaml |
Build Docker image and deploy to ECS via templated taskdef. |
npm_build_deploy_default.yaml |
npm install + build + Docker image + ECS deploy using the repo's root Dockerfile. |
Supporting scripts for the ECS pipeline live under Support/
(taskdef templating, OpenAPI condensing, terraform plan automation).
-
The logic needs the calling job's environment. Composite actions run as steps in the caller's job, so
env:vars, secrets composed by the trigger, and the checked-out file system are all present automatically.actions/ciuses this to receive test env vars (RPC URLs, API keys) without any secret-passing. -
The output feeds another job in the same trigger file.
actions/docker-testresolves service metadata and outputs it for thereleasejob to consume — this requires sharing state within the same workflow run, not a separate runner. -
The logic is a sequence of shell steps with no complex job-level concerns (no matrix, no needs:, no job-level permissions beyond what the caller grants).
-
The logic needs its own isolated runner. Reusable workflows spin up a fresh runner with their own environment.
apps-changeset-check.ymlandapps-npm-release.ymlneed their own checkout of the calling repo, their ownpnpm install, and their own job-level configuration. -
The logic is a complete, self-contained unit that should appear as a distinct check in the PR status (e.g. "Changeset check / Require changeset"). Composite action steps roll up into the calling job's check, not their own.
-
The logic calls another reusable workflow (only workflows can call workflows — composite actions cannot).
apps-docker-release.ymldelegates togcp_pipeline_release_image.yaml, which is itself a reusable workflow.
- Add
.github/workflows/apps-<name>.ymlwithon: workflow_call:as the only trigger. - Declare required secrets in the
workflow_call: secrets:block. - Add
permissions:at the workflow level with least-privilege scopes. - Keep all job logic in this file — trigger files must be thin wrappers.
- Add a trigger file template (
apps-<name>-trigger.yml) and update the tables above.
Trigger file permissions: A trigger file's permissions: must be a superset of
every scope the called workflow's jobs declare. Mismatches cause a startup_failure
before any step runs. The canonical trigger files already have the correct
permissions.
- Create
.github/actions/<name>/action.ymlwithusing: composite. - No dist or build step needed — GitHub executes composite action steps directly.
- Add the action to the tables above.
Required when the action is called from inside a reusable workflow that runs
cross-repo — raw scripts under .github/scripts/ are not accessible in the calling
repo's workspace.
- Create
packages/<name>/withsrc/index.ts,package.json, andtsconfig.json(see existing packages — the per-package tsconfig is required for ncc compatibility). - Create
.github/actions/<name>/action.ymlreferencingdist/index.js. - Add
"build:<name>": "ncc build packages/<name>/src/index.ts -o .github/actions/<name>/dist"to rootpackage.json. - Run
pnpm run build:<name>locally and commit thedist/. - Add
.github/actions/<name>/dist/** -diff linguist-generated=trueto.gitattributes. - Add a job to
apps-build-actions.ymlso the dist rebuilds automatically onmain.