Skip to content

feat(ci): release pipeline + stop tracking dist on main#16

Merged
FedeZara merged 6 commits into
mainfrom
FedeZara/release-pipeline
May 15, 2026
Merged

feat(ci): release pipeline + stop tracking dist on main#16
FedeZara merged 6 commits into
mainfrom
FedeZara/release-pipeline

Conversation

@FedeZara
Copy link
Copy Markdown
Contributor

@FedeZara FedeZara commented May 14, 2026

Stacked on top of #15. Set the base to FedeZara/fer-10253-obs-foundations so the diff scopes to just the release-pipeline work; once #15 lands the base will auto-retarget to main.

Summary

Replaces the old release.yml (which only moved alias tags on GitHub Release publish) with a workflow_dispatch pipeline that builds an action with secrets baked in, uploads sourcemaps to Sentry, and publishes the artifact to a per-action orphan branch dist/<action>. Also removes dist/index.js from main since artifacts now live on the dist branches.

What's in this PR

Commit 1 — feat(ci): add release workflow with build-time env injection and Sentry sourcemap upload

The new pipeline (workflow_dispatch only, no automatic triggers):

  1. validate — semver regex on the version input, reject example-action, hard-fail if the tag already exists (tags are immutable)
  2. buildpnpm install → build shared → build the selected action with RELEASE_TAG=<action>@<version>, POSTHOG_API_KEY, SENTRY_DSN_AUTOMATIONS, AUTOMATION_EVENT_API_URL exported as env vars. Asserts RELEASE_TAG was substituted into the bundle (catches the case where injection silently no-ops)
  3. sentry-releasegetsentry/action-release@v1 creates the release and uploads sourcemaps with url_prefix: '~/'. Gated on SENTRY_AUTH_TOKEN being present so the pipeline runs end-to-end before Sentry is provisioned
  4. publish-dist — checkout or orphan-create dist/<action>, copy in the freshly built dist/ + action.yml, commit with Source: / Built from: body, push, then tag <action>@<version> on that commit
  5. alias-tags — move <action>@v<major> and <action>@v<major>.<minor> aliases (skipped on prerelease)
  6. github-releasegh release create against the new tag
  7. main-release-marker — runs scripts/append-release-entry.mjs to prepend an entry to <action>/RELEASES.md on main, then commits chore(release): <action>@<version>. Best-effort: a failed push (e.g. from branch protection) logs a warning, doesn't roll back the release

Concurrency: group: release-${{ inputs.action }}, cancel-in-progress: false. Two parallel releases of different actions are fine; two of the same action serialize.

Build-time env injection

packages/shared/src/telemetry/build-constants.ts now reads process.env.X ?? <fallback> instead of being hardcoded. tsup's env option substitutes those reads with string literals at bundle time (shared between all tsup configs via scripts/build-env.ts). Local dev / tests / non-release CI: env vars unset → fallbacks → telemetry no-ops. Release CI: env vars set → values baked into the bundle.

New constant: RELEASE_TAG (defaults to \"dev\"). Wired into:

  • Sentry init({ release }) for stack-trace deobfuscation
  • PostHog $lib_version + actions_version properties
  • Log payload actions_version field
  • Lightweight API event body actions_version

Per-action RELEASES.md ledger

scripts/append-release-entry.mjs maintains a human-readable per-action release log on main:

<!-- AUTO-GENERATED — DO NOT EDIT BY HAND. ... -->

# verify-token releases

## v1.0.1 — 2026-05-08
- Tag: `verify-token@v1.0.1`
- Source: [`a1b2c3d`](...) on main
- Dist: [`x9y8z7w`](...) on `dist/verify-token`
- Sentry release: `verify-token@v1.0.1`
- GitHub Release: ...

Per-action files (not a single repo-root file) so concurrent releases of different actions never race on the same file. Idempotent: re-running with an already-recorded version is a no-op.

Commit 2 — build: stop tracking dist/index.js on main

  • git rm 9 tracked dist/index.js files (one per Node.js action, including example-action)
  • .gitignore: replace the old "must commit dist" rule with */dist/ (action dirs) + packages/*/dist/ (shared package)
  • ci.yml: drop the now-obsolete "Check dist is committed" step and expand the bundle-existence check to cover all 9 actions (was hardcoded to 4 — example-action preview upgrade verify — silently skipping the other 5)

Design decisions worth flagging

  • Per-action orphan branches (dist/setup-cli, dist/sync-openapi, …) instead of a single shared dist branch. Cleaner per-action git log, safer under concurrent releases, and sets up sibling-ref pinning cleanly for composite actions. See the plan file for the full rationale (alternatives considered: ephemeral per-release branches, shared dist branch).
  • Source SHA recorded in dist commit bodies (Source: <main-sha>) so the orphan-history boundary doesn't lose source traceability. Plus per-action RELEASES.md on main as a second, human-readable index.
  • Marker commit format mirrors fern CLI's (chore(cli): release X.Y.Z) for org-idiom consistency.
  • Sentry release is created before the dist branch push so a failed sourcemap upload aborts cleanly with no orphan tag.

Backward compatibility

Zero existing tags in this repo (verified with git tag -l and git ls-remote --tags origin), so nothing to break. Hypothetically, if old tags existed, they'd keep working because they're immutable pointers to historical commits that still have dist files in their trees. The only thing that breaks: consumers using uses: fern-api/actions/<action>@main directly — they should pin to a tag.

Required repo configuration before first real release

Type Name Notes
Secret SENTRY_AUTH_TOKEN Sentry user/org token with project:releases scope. Without it, the sentry-release job auto-skips
Variable SENTRY_ORG e.g. buildwithfern
Variable SENTRY_PROJECT e.g. automations-actions
Variable POSTHOG_API_KEY Optional — leave empty to keep PostHog as a no-op
Variable SENTRY_DSN_AUTOMATIONS Optional — leave empty to keep Sentry capture as a no-op
Variable AUTOMATION_EVENT_API_URL Optional — leave empty to keep the Lightweight API as a no-op

Test plan

  • Local: typecheck, lint, test (58/58 shared), build all 9 actions, actionlint clean on all workflows
  • Local: smoke-tested env injection — RELEASE_TAG=verify-token@v0.0.1-test pnpm build produces a bundle containing that literal; default build contains the \"dev\" fallback
  • Local: scripts/append-release-entry.mjs smoke-tested for fresh write, prepend, and idempotent retry
  • Dispatch the workflow against verify-token with version=v0.0.1-test1, prerelease=true (no Sentry secret) — verify orphan branch creation, tag push, ledger entry, GitHub Release
  • Verify env injection in the published bundle: git show verify-token@v0.0.1-test1:verify-token/dist/index.js | grep -F 'verify-token@v0.0.1-test1'
  • Consumer test: scratch workflow with uses: fern-api/actions/verify-token@v0.0.1-test1 runs end-to-end
  • Add Sentry secret + vars, dispatch with v0.0.1-test2, verify sourcemap upload and stack-trace deobfuscation on a forced error
  • Tag-collision check: re-dispatch the same version, expect validate-step hard-fail
  • Cleanup test tags / dist branches / releases

Out of scope (follow-ups)

  • Sibling-ref auto-pinning for composite actions (generate/action.yml references verify-token@main today — the new workflow has the seam to sed-rewrite this at release time, but the auto-resolution to "latest stable sibling" isn't implemented)
  • Build provenance (actions/attest-build-provenance) — worth adding for supply-chain, tracked separately
  • Auto-merge PR for the main ledger marker if branch protection on main rejects direct pushes (the workflow logs a warning and continues today)

Open in Devin Review

@FedeZara FedeZara requested a review from Swimburger as a code owner May 14, 2026 13:59
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment thread .gitignore
# workflow for how dist/index.js + dist/index.js.map land on the dist branch.
# Local `pnpm build` still produces dist/ for typecheck/tests; just don't
# commit it.
*/dist/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 CONTRIBUTING.md rules violated: new */dist/ gitignore contradicts "commit dist/" instructions

The PR adds */dist/ to .gitignore (line 14), which makes it impossible to follow the CONTRIBUTING.md instructions that explicitly say to "Run pnpm build and commit the generated dist/" (Node.js action step 6) and "Commit dist/ like a normal Node.js action" (Hybrid action step 5). The CONTRIBUTING.md also describes a release process (manual git tag + gh release create triggering the release workflow) that no longer applies now that release.yml is workflow_dispatch. Since CONTRIBUTING.md is a mandatory rule file, the PR should update it to reflect the new architecture (orphan dist branches, no committed bundles on main, new workflow_dispatch release process).

Prompt for agents
The PR adds `*/dist/` to .gitignore, which means dist/ folders can no longer be committed to main. This directly contradicts the CONTRIBUTING.md instructions in multiple places:

1. Node.js action step 6: Run pnpm build and commit the generated dist/
2. Hybrid action step 5: Commit dist/ like a normal Node.js action
3. The Releasing section describes manually creating tags and GitHub releases, but the workflow is now workflow_dispatch
4. The repository structure comment says release.yml Runs on release publish but it now runs on workflow_dispatch
5. The Required secrets section mentions ACTIONS_RELEASE_TOKEN but the new workflow uses GITHUB_TOKEN

CONTRIBUTING.md needs to be updated to describe the new architecture: dist bundles are published to orphan branches dist/<action> via the release workflow, not committed to main. The release process is now triggered via workflow_dispatch in the GitHub Actions UI. Contributors should NOT commit dist/ to main.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread .github/workflows/release.yml Outdated
Comment on lines +11 to +12
# See /Users/fedezara/.claude/plans/help-me-setting-up-velvety-sutherland.md
# for design rationale (orphan branches, env-var injection, ledger format).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Local developer filesystem path leaked in release.yml comment

Line 11 of the release workflow contains # See /Users/fedezara/.claude/plans/help-me-setting-up-velvety-sutherland.md — a hardcoded reference to a developer's local filesystem path. This is meaningless to anyone else reading the file and leaks the developer's username and local tooling details into the repository.

Suggested change
# See /Users/fedezara/.claude/plans/help-me-setting-up-velvety-sutherland.md
# for design rationale (orphan branches, env-var injection, ledger format).
# See the PR description for design rationale (orphan branches, env-var
# injection, ledger format).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@FedeZara FedeZara force-pushed the FedeZara/release-pipeline branch from b11b5c0 to 7cb687e Compare May 14, 2026 15:13
FedeZara added 2 commits May 14, 2026 17:17
…ry sourcemap upload

Replaces release.yml (which only moved alias tags on GitHub Release publish)
with a workflow_dispatch pipeline that:

  - Bakes RELEASE_TAG / POSTHOG_API_KEY / SENTRY_DSN_AUTOMATIONS /
    AUTOMATION_EVENT_API_URL into the bundle via tsup's `env` option
  - Builds the action with sourcemaps enabled
  - Uploads sourcemaps to Sentry, tied to a release named <action>@<version>
  - Publishes the artifact to an orphan branch `dist/<action>` with a
    commit linking back to the source SHA on main
  - Tags <action>@<version> on the dist branch commit
  - Moves floating <action>@v<major> / <action>@v<major>.<minor> aliases
    (skipped for prereleases)
  - Appends an entry to <action>/RELEASES.md on main and pushes a
    `chore(release): <action>@<version>` marker commit (best-effort; logs
    a warning if branch protection rejects the push)
  - Creates a GitHub Release for the new tag

Telemetry constants now read from process.env with no-op fallbacks so local
dev / tests / non-release CI keep working. RELEASE_TAG is wired through to
Sentry.init({ release }), PostHog $lib_version, and the actions_version
field on log payloads + Lightweight API events.

Sourcemaps (*/dist/*.map) are gitignored on main — they ride along on the
dist branches and are uploaded to Sentry per release.

See /Users/fedezara/.claude/plans/help-me-setting-up-velvety-sutherland.md
for design rationale (per-action orphan branches over single shared dist;
RELEASES.md ledger format; sibling-ref pinning roadmap).
Action bundles are now published to per-action orphan branches
`dist/<action>` by the release workflow added in the previous commit.
Consumers should pin to a tag (e.g. setup-cli@v1) — those tags point at
commits on the dist branches that contain the artifact. Direct
`uses: fern-api/actions/<action>@main` references won't resolve anymore.

Changes:
  - `git rm` 9 tracked dist/index.js files (one per Node.js action,
    including example-action). Pre-existing history of these files is
    preserved on the rebase ancestor — `git log --follow` still works.
  - .gitignore: replace the old "must commit dist" rule with `*/dist/`
    (action dirs) + `packages/*/dist/` (shared package).
  - ci.yml: drop the "Check dist is committed" step (no longer applies)
    and expand the "Verify bundles exist" check to cover all 9 actions
    (was hardcoded to 4 — `example-action preview upgrade verify` — and
    silently skipping the other 5).

Backward compatibility: no existing tags in this repo, so nothing to
break. If tags had existed, they'd keep working because they're
immutable pointers to historical commits that still have dist files in
their trees.
@FedeZara FedeZara changed the base branch from FedeZara/fer-10253-obs-foundations to graphite-base/16 May 14, 2026 15:18
@FedeZara FedeZara force-pushed the FedeZara/release-pipeline branch from 7cb687e to f47ab0f Compare May 14, 2026 15:18
@FedeZara FedeZara changed the base branch from graphite-base/16 to main May 14, 2026 15:18
Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

FedeZara added 4 commits May 15, 2026 18:19
The old doc was inconsistent with reality in several places:
  - Repo structure: claimed setup-cli, resolve-cli, verify-token were
    composite actions and described preview as hybrid. All 8 production
    actions are Node.js (`runs.using: node20`, `runs.main: dist/index.js`)
    since the conversion in PR #12.
  - "Adding a new action" Node.js step 6 + hybrid step 5 told contributors
    to commit dist/. Dist is now gitignored on main; bundles are published
    to per-action orphan branches by the release workflow.
  - "Releasing" section described the manual `git tag` + `gh release create`
    flow. Releases are now workflow_dispatch.
  - Required secrets section listed ACTIONS_RELEASE_TOKEN. The new pipeline
    uses Sentry/PostHog vars + secret instead.
  - release.yml is no longer "Runs on release publish".

Also drops the composite + hybrid action sections — no current actions
use those patterns; they're cleaner to add back if/when needed.
With dist/ no longer tracked on main, two CI jobs broke:

  - `Lint GitHub Actions`: actionlint statically resolves `runs.main:
    dist/index.js` against the working tree. With dist gone, every local
    `uses: ./<action>` reference (currently `uses: ./resolve-cli` in
    test-resolve-cli.yml) fails the action-existence check.
  - `Test resolve-cli`: the matrix uses `uses: ./resolve-cli`, which
    crashes at runtime with "File not found: ./resolve-cli/dist/index.js".

Fix: add `pnpm install` + `pnpm build` steps before both. The actionlint
job builds all actions so any future `uses: ./<action>` reference also
resolves; the resolve-cli test only builds shared + resolve-cli.

Also rename the Sentry secret reference to `FERN_SENTRY_AUTH_TOKEN` so the
same secret value used by the fern repo's publish workflows covers this
repo too. The in-container env var stays `SENTRY_AUTH_TOKEN` (what
sentry-cli and getsentry/action-release@v1 look for).
@FedeZara FedeZara merged commit 4b6042b into main May 15, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants