Skip to content
Merged
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
149 changes: 127 additions & 22 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,38 +1,81 @@
name: Release

on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: Release version in x.y.z format
required: true
type: string

permissions:
actions: read
contents: write
id-token: write

concurrency:
group: release-${{ github.ref_name }}
group: release-main
cancel-in-progress: false

jobs:
release:
runs-on: ubuntu-latest
timeout-minutes: 20
timeout-minutes: 30
env:
RELEASE_VERSION: ${{ inputs.version }}

steps:
- name: Create release app token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: ttdash

- name: Check out repository
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: main
token: ${{ steps.app-token.outputs.token }}
persist-credentials: false

- name: Verify tagged commit is on main
run: |
git fetch origin main --depth=1
git branch -r --contains HEAD | grep -q 'origin/main'

- name: Verify tag matches package version
- name: Verify release version input
run: |
VERSION="$(node -p "require('./package.json').version")"
test "v${VERSION}" = "${GITHUB_REF_NAME}"
export CURRENT_VERSION="$(node -p "require('./package.json').version")"
export RELEASE_VERSION
echo "CURRENT_VERSION=${CURRENT_VERSION}" >> "$GITHUB_ENV"
node - <<'NODE'
const current = process.env.CURRENT_VERSION
const requested = process.env.RELEASE_VERSION
const semverPattern = /^\d+\.\d+\.\d+$/

if (!semverPattern.test(requested)) {
throw new Error(`Release version must use x.y.z format. Received: ${requested}`)
}

const parse = (value) => value.split('.').map((part) => Number.parseInt(part, 10))
const compare = (left, right) => {
for (let index = 0; index < 3; index += 1) {
if (left[index] > right[index]) return 1
if (left[index] < right[index]) return -1
}
return 0
}

if (compare(parse(requested), parse(current)) < 0) {
throw new Error(`Release version ${requested} must not be lower than current package version ${current}`)
}
NODE
echo "RELEASE_TAG=v${RELEASE_VERSION}" >> "$GITHUB_ENV"
if [[ "${RELEASE_VERSION}" = "${CURRENT_VERSION}" ]]; then
echo "SHOULD_BUMP=false" >> "$GITHUB_ENV"
echo "Release version already present on main. Continuing in retry mode."
else
echo "SHOULD_BUMP=true" >> "$GITHUB_ENV"
fi

- name: Set up Node.js
uses: actions/setup-node@v6
Expand All @@ -41,6 +84,34 @@ jobs:
cache: npm
registry-url: https://registry.npmjs.org

- name: Verify main CI succeeded
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
MAIN_SHA="$(git rev-parse HEAD)"
node scripts/verify-main-ci.js \
--repo "${{ github.repository }}" \
--workflow ci.yml \
--branch main \
--sha "${MAIN_SHA}" \
--retries 90 \
--retry-delay-ms 10000
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Configure git identity
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Configure authenticated remote
env:
APP_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
git remote set-url origin "https://x-access-token:${APP_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"

- name: Bump package version
if: env.SHOULD_BUMP == 'true'
run: npm version "${RELEASE_VERSION}" --no-git-tag-version

- name: Install dependencies
run: npm ci --ignore-scripts

Expand All @@ -64,14 +135,49 @@ jobs:
with:
bun-version: latest

- name: Create release commit and tag
run: |
git fetch --tags origin
if [[ "${SHOULD_BUMP}" = "true" ]]; then
git add package.json package-lock.json
git commit -m "v${RELEASE_VERSION}: Release"
fi

if git rev-parse "${RELEASE_TAG}" >/dev/null 2>&1; then
echo "Tag ${RELEASE_TAG} already exists locally. Skipping tag creation."
else
git tag -a "${RELEASE_TAG}" -m "${RELEASE_TAG}"
fi

- name: Push release commit and tag
run: |
if [[ "${SHOULD_BUMP}" = "true" ]]; then
git push origin HEAD:main
fi

if git ls-remote --tags origin "refs/tags/${RELEASE_TAG}" | grep -q .; then
echo "Tag ${RELEASE_TAG} already exists on origin. Skipping tag push."
else
git push origin "${RELEASE_TAG}"
fi

- name: Detect existing npm publication
run: |
if npm view "@roastcodes/ttdash@${RELEASE_VERSION}" version >/dev/null 2>&1; then
echo "NPM_VERSION_EXISTS=true" >> "$GITHUB_ENV"
echo "Package version already exists on npm. Skipping publish."
else
echo "NPM_VERSION_EXISTS=false" >> "$GITHUB_ENV"
fi

- name: Publish package to npm
if: env.NPM_VERSION_EXISTS != 'true'
run: npm publish
Comment thread
tyl3r-ch marked this conversation as resolved.

- name: Wait for npm registry propagation
run: |
VERSION="$(node -p "require('./package.json').version")"
for attempt in 1 2 3 4 5 6; do
if npm view "@roastcodes/ttdash@${VERSION}" version >/dev/null 2>&1; then
if npm view "@roastcodes/ttdash@${RELEASE_VERSION}" version >/dev/null 2>&1; then
exit 0
fi
sleep 10
Expand All @@ -81,23 +187,22 @@ jobs:

- name: Verify registry install paths
run: |
VERSION="$(node -p "require('./package.json').version")"
node scripts/verify-registry-install.js \
--package @roastcodes/ttdash \
--version "${VERSION}" \
--version "${RELEASE_VERSION}" \
--retries 6 \
--retry-delay-ms 10000

- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
if gh release view "${GITHUB_REF_NAME}" >/dev/null 2>&1; then
echo "Release ${GITHUB_REF_NAME} already exists. Skipping creation."
if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then
echo "Release ${RELEASE_TAG} already exists. Skipping creation."
exit 0
fi

gh release create "${GITHUB_REF_NAME}" \
gh release create "${RELEASE_TAG}" \
--verify-tag \
--title "${GITHUB_REF_NAME}" \
--title "${RELEASE_TAG}" \
--generate-notes
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

## [Unreleased]

## [6.1.4] - 2026-04-11

### Added
- **GitHub-driven release flow** — releases can now be started manually from GitHub Actions with a target version input, instead of relying on a locally created tag on `main`
- **CI release gate** — the release workflow now verifies that the latest `CI` run for the current `main` commit completed successfully before any version bump, tag, or npm publish step begins
- **Release app verification** — a dedicated GitHub API helper now validates the `CI` precondition directly from the workflow, so release gating stays tied to the exact `main` SHA

### Improved
- **Single human-managed version source** — the frontend app version is now injected from `package.json` at build time instead of being maintained as a second manual version constant
- **Protected-branch compatibility** — the release workflow now uses the dedicated `ttdash-release` GitHub App token for checkout, push, tag creation, and GitHub release creation, so the release path works cleanly with branch rules and ruleset bypasses
- **Release recovery behavior** — rerunning a failed release with the same version now resumes cleanly when the version bump commit, tag, or npm publication already exists
- **Release documentation** — the maintainer guide now documents the GitHub App setup, ruleset expectations, workflow-dispatch release path, and the new post-publish verification model

## [6.1.0] - 2026-04-11

### Added
Expand Down
73 changes: 37 additions & 36 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,61 +9,62 @@ Before the first public release, configure npm Trusted Publishing for this repos
3. Ensure you have publish rights in the `roastcodes` npm organization
4. In npm package settings, add this GitHub repository as a trusted publisher for `@roastcodes/ttdash`
5. Confirm the GitHub Actions release workflow is allowed to request an OIDC token
6. Install the `ttdash-release` GitHub App on `roastcodes/ttdash`
7. Add `APP_ID` and `APP_PRIVATE_KEY` as Actions secrets for this repository
8. Add the `ttdash-release` GitHub App as a bypass actor in the `main` ruleset

Trusted Publishing is preferred because it avoids long-lived npm tokens and enables provenance for public publishes.

If you want npm provenance on the published package, the GitHub repository must be public when the release workflow runs.

## Release Checklist

1. Update `package.json` version
2. Add the matching section to `CHANGELOG.md`
3. Run the full local verification suite:
## GitHub Repository Setup

```bash
npm run test:unit:coverage
npm run build
npm run verify:package
PLAYWRIGHT_TEST_PORT=3016 npm_config_cache=/tmp/ttdash-npm-cache npm run test:e2e
```
Before using the manual release workflow, make sure:

4. Merge the release changes to `main`
5. Create and push a tag that matches the package version exactly
1. `main` is protected and requires the `CI` status check before merges
2. CodeQL is enabled in the GitHub UI if you want it as a manual release gate
3. the `ttdash-release` GitHub App is allowed to push the version-bump commit and annotated tag back to `main`

Example:
If branch protection or rulesets block the `ttdash-release` app from writing to `main` or pushing `v*` tags, the workflow will fail when it tries to push the release commit or tag.

```bash
bash scripts/tag-main-release.sh
```
## Release Checklist

Optional explicit version:
1. Merge the intended release state to `main`
2. Confirm the latest `CI` run on `main` succeeded
3. Confirm CodeQL is green in the GitHub UI
4. Start the `Release` workflow manually from GitHub Actions and provide the target version in `x.y.z` format

```bash
bash scripts/tag-main-release.sh 6.1.3
```

Dry-run preview:
Optional local confidence check before starting the workflow:

```bash
bash scripts/tag-main-release.sh --dry-run
npm run test:unit:coverage
npm run build
npm run verify:package
PLAYWRIGHT_TEST_PORT=3016 npm_config_cache=/tmp/ttdash-npm-cache npm run test:e2e
```

## What the Release Workflow Does

On a `v*` tag push, the workflow:

1. verifies the tagged commit is on `main`
2. verifies the tag matches `package.json`
3. runs unit/integration tests with coverage
4. builds the production bundle
5. verifies the packed npm artifact
6. runs the Playwright smoke suite
7. publishes `@roastcodes/ttdash` to npm through Trusted Publishing
8. waits for npm registry propagation
9. verifies:
On a manual `workflow_dispatch` run against `main`, the workflow:

1. verifies the requested version is greater than the current `package.json` version
or resumes a partially completed release when the requested version is already on `main`
2. verifies the latest `CI` run for the current `main` commit succeeded
3. bumps `package.json` and `package-lock.json` to the requested version
4. runs unit/integration tests with coverage
5. builds the production bundle
6. verifies the packed npm artifact
7. runs the Playwright smoke suite
8. creates and pushes the release commit and annotated tag
9. publishes `@roastcodes/ttdash` to npm through Trusted Publishing
10. waits for npm registry propagation
11. verifies:
- `npx --yes @roastcodes/ttdash@<version> --help`
- `bunx @roastcodes/ttdash@<version> --help`
10. creates the GitHub release
12. creates the GitHub release

Note: the workflow reruns the release-critical test suite itself after the version bump. This is necessary because the workflow-created push back to `main` should not be relied on to trigger the normal `CI` workflow again.
If a release fails after the version bump was already pushed, rerunning the workflow with the same version resumes that release instead of forcing another version bump.

## Post-Publish Checks

Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
"test:e2e:ci": "playwright test",
"test:all": "npm run test:unit && npm run test:e2e",
"pack:dry-run": "npm pack --dry-run",
"tag:main-release": "bash scripts/tag-main-release.sh",
"verify:package": "node scripts/verify-package.js",
"verify:registry-install": "node scripts/verify-registry-install.js",
"verify:release": "npm run test:unit:coverage && npm run build && npm run verify:package",
Expand Down
Loading
Loading