Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .changeset/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Changesets

This directory holds [changesets](https://github.com/changesets/changesets): one
markdown file per set of changes, declaring how they bump the version and a
human-readable summary that becomes the `CHANGELOG.md` entry.

`transitland-lib` is a Go project; changesets is used only to track changes and
drive versioning/releases. Versions stay continuous with the existing `vX.Y.Z`
git tags (changesets uses the `v`-prefixed format for single-package repos).

## Adding a changeset (in your feature PR)

```bash
pnpm changeset
```

Pick the bump level (patch / minor / major) and write a short summary. Commit
the generated `.changeset/<name>.md` file with your PR.

## What happens next (automated in CI)

1. When your PR merges to `main`, a bot opens/updates a **"Version Packages"** PR
that consumes pending changesets, bumps `package.json`, and writes
`CHANGELOG.md`.
2. Merging that PR creates the `vX.Y.Z` git tag and GitHub Release, which builds
and ships the binaries.

You do not run `changeset version` or `changeset tag` by hand; CI does.

See [`RELEASING.md`](../RELEASING.md) for the full flow and the dependency
hardening policy.
11 changes: 11 additions & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.4/schema.json",
"changelog": ["@changesets/changelog-github", { "repo": "interline-io/transitland-lib" }],
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
5 changes: 5 additions & 0 deletions .changeset/release-tooling-changesets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"transitland-lib": patch
---

Adopt changesets for versioning and a consolidated, automated release workflow (continuous with the existing vX.Y.Z tags). Hardened with SHA-pinned actions, a committed pnpm lockfile, an install cooldown, and no dependency lifecycle scripts. Releases now publish SLSA build provenance and a SHA256SUMS checksum file, and binaries are built with -trimpath.
185 changes: 129 additions & 56 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,34 +1,89 @@
name: Release

# Consolidated release pipeline driven by changesets.
#
# Triggered only after the Test Suite finishes successfully on main (so a release
# never races ahead of tests). One of two things happens per run:
#
# * Pending changesets exist -> the changesets action opens/updates the
# "Version Packages" PR (bumps package.json, writes CHANGELOG.md). No release.
# * No changesets, and the committed version has no git tag yet (i.e. the
# "Version Packages" PR was just merged) -> build, sign, tag, release, and
# dispatch the Homebrew update.
#
# Because everything happens in this single workflow, the built-in GITHUB_TOKEN
# is sufficient for all in-repo steps (creating the Release here does not need to
# trigger any other workflow). The GitHub App token is used ONLY for the
# cross-repo Homebrew dispatch, exactly as before.
#
# External actions are pinned to commit SHAs; bump deliberately.

on:
release:
types: [created]
workflow_run:
workflows: ["Test Suite"]
types: [completed]
branches: [main]

Comment thread
drewda marked this conversation as resolved.
# Serialize release runs so two Test Suite completions finishing close together
# can't race on the Version Packages PR or on tag/release creation. Do not cancel
# an in-flight run mid-release.
concurrency:
group: release
cancel-in-progress: false

jobs:
check-tests:
changesets:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
contents: write
pull-requests: write
outputs:
release: ${{ steps.detect.outputs.release }}
version: ${{ steps.detect.outputs.version }}
steps:
- name: Check Test Suite Status
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
script: |
const { data: workflows } = await github.rest.actions.listWorkflowRunsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'test.yml',
head_sha: context.sha,
status: 'completed'
});
const testRun = workflows.workflow_runs.find(run => run.conclusion === 'success');
if (!testRun) {
core.setFailed('Test Suite workflow must pass before release can be built');
}
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0
- name: Set up pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
with:
run_install: false
- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
cache: pnpm
- name: Install release tooling
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Create or update Version Packages PR
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0
with:
version: pnpm changeset:version
title: "Version Packages"
commit: "Version Packages"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Detect release
id: detect
# Read the committed version (HEAD), not any working-tree mutation left by
# the changesets action. Release only when that version has no tag yet.
run: |
VERSION="$(git show HEAD:package.json | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version")"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
if git rev-parse -q --verify "refs/tags/v$VERSION" >/dev/null; then
echo "release=false" >> "$GITHUB_OUTPUT"
echo "v$VERSION already tagged; nothing to release."
else
echo "release=true" >> "$GITHUB_OUTPUT"
echo "v$VERSION is not tagged yet; will build and release."
fi

build-linux:
needs: check-tests
needs: changesets
if: ${{ needs.changesets.outputs.release == 'true' }}
runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -38,21 +93,24 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.workflow_run.head_sha }}
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{ matrix.go-version }}
- name: Build on Linux
working-directory: ${{ github.workspace }}/cmd/transitland
run: CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -buildvcs=true -ldflags "-X main.tag=$(git describe --tags --abbrev=0)"
run: CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -trimpath -buildvcs=true -ldflags "-s -w -X main.tag=v${{ needs.changesets.outputs.version }}"
- name: Store Linux binary
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: transitland-linux
path: ${{ github.workspace }}/cmd/transitland/transitland

build-macos-intel:
needs: check-tests
needs: changesets
if: ${{ needs.changesets.outputs.release == 'true' }}
runs-on: macos-15-large # macOS on Intel
permissions:
contents: read
Expand All @@ -62,13 +120,15 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.workflow_run.head_sha }}
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{ matrix.go-version }}
- name: Build on macOS
working-directory: ${{ github.workspace }}/cmd/transitland
run: CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -buildvcs=true -ldflags "-X main.tag=$(git describe --tags --abbrev=0)"
run: CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -trimpath -buildvcs=true -ldflags "-s -w -X main.tag=v${{ needs.changesets.outputs.version }}"
- name: Import Code-Signing Certificates
uses: Apple-Actions/import-codesign-certs@5142e029c445c10ffc7149d172e540235a065466 # v7.0.0
with:
Expand All @@ -93,7 +153,8 @@ jobs:
path: ${{ github.workspace }}/transitland.zip

build-macos-apple:
needs: check-tests
needs: changesets
if: ${{ needs.changesets.outputs.release == 'true' }}
runs-on: macos-15 # macOS on Apple Silicon
permissions:
contents: read
Expand All @@ -103,13 +164,15 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.workflow_run.head_sha }}
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{ matrix.go-version }}
- name: Build on macOS
working-directory: ${{ github.workspace }}/cmd/transitland
run: CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -buildvcs=true -ldflags "-X main.tag=$(git describe --tags --abbrev=0)"
run: CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -trimpath -buildvcs=true -ldflags "-s -w -X main.tag=v${{ needs.changesets.outputs.version }}"
- name: Import Code-Signing Certificates
uses: Apple-Actions/import-codesign-certs@5142e029c445c10ffc7149d172e540235a065466 # v7.0.0
with:
Expand All @@ -134,11 +197,18 @@ jobs:
path: ${{ github.workspace }}/transitland.zip

release:
needs: [build-linux, build-macos-intel, build-macos-apple]
needs: [changesets, build-linux, build-macos-intel, build-macos-apple]
if: ${{ needs.changesets.outputs.release == 'true' }}
runs-on: ubuntu-latest
permissions:
contents: write
contents: write # create the tag, release, and upload assets
id-token: write # OIDC identity for keyless provenance signing
attestations: write # record the build provenance attestation
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.workflow_run.head_sha }}
- name: Download Linux binary
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
Expand All @@ -154,41 +224,44 @@ jobs:
with:
name: transitland-macos-apple
path: transitland-macos-apple
- name: Unzip and rename macOS Intel binary
- name: Assemble artifacts and checksums
# Flatten the three binaries into dist/ with their final asset names and
# produce SHA256SUMS (basenames only, so `sha256sum -c` works after download).
run: |
mkdir -p dist
unzip -j transitland-macos-intel/transitland.zip -d transitland-macos-intel/
mv transitland-macos-intel/transitland transitland-macos-intel/transitland-macos-intel
- name: Unzip and rename macOS Apple Silicon binary
run: |
unzip -j transitland-macos-apple/transitland.zip -d transitland-macos-apple/
mv transitland-macos-apple/transitland transitland-macos-apple/transitland-macos-apple
- name: Copy and rename Linux binary
run: cp transitland-linux/transitland transitland-linux/transitland-linux
- name: Upload Release Assets
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
files: |
transitland-linux/transitland-linux
transitland-macos-intel/transitland-macos-intel
transitland-macos-apple/transitland-macos-apple
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

release-notes:
needs: release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Generate release notes
cp transitland-linux/transitland dist/transitland-linux
cp transitland-macos-intel/transitland dist/transitland-macos-intel
cp transitland-macos-apple/transitland dist/transitland-macos-apple
( cd dist && sha256sum transitland-linux transitland-macos-intel transitland-macos-apple > SHA256SUMS )
cat dist/SHA256SUMS
- name: Attest build provenance
# SLSA build provenance for every binary listed in SHA256SUMS. Verify with:
# gh attestation verify <file> --repo interline-io/transitland-lib
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-checksums: dist/SHA256SUMS
- name: Create tag and GitHub Release with assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG: ${{ github.event.release.tag_name }}
run: gh release edit "$TAG" --generate-notes
VERSION: v${{ needs.changesets.outputs.version }}
TARGET: ${{ github.event.workflow_run.head_sha }}
# Extract the notes for this version from CHANGELOG.md (the first
# "## <version>" section). gh creates the tag at $TARGET atomically.
run: |
NOTES="$(awk '/^## /{n++} n==1{print} n==2{exit}' CHANGELOG.md)"
gh release create "$VERSION" --target "$TARGET" \
--title "$VERSION" \
--notes "${NOTES:-Release $VERSION}" \
dist/transitland-linux \
dist/transitland-macos-intel \
dist/transitland-macos-apple \
dist/SHA256SUMS

trigger-homebrew-update:
needs: release
needs: [changesets, release]
if: ${{ needs.changesets.outputs.release == 'true' }}
runs-on: ubuntu-latest
permissions:
actions: write
Expand All @@ -215,6 +288,6 @@ jobs:
workflow_id: 'update-formula.yml',
ref: 'main',
inputs: {
version: '${{ github.event.release.tag_name }}'
version: 'v${{ needs.changesets.outputs.version }}'
}
});
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ tldb/test.db
**/.DS_Store
tmp
**/tmp
transitland
transitland

# Node / changesets release tooling
node_modules/
2 changes: 2 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Defense in depth: never execute dependency lifecycle scripts on install.
ignore-scripts=true
Loading