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
13 changes: 13 additions & 0 deletions .github/actions/checks/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ runs:
shell: bash
run: npm ci

# Structure fails here; style prints warnings on the pull request.
- name: Check the changelog
shell: bash
run: node scripts/changelog-check.js

# A release must also agree with itself: the newest dated section, the
# package version and the demo pin all name one version, and nothing is left
# under [Unreleased].
- name: Check the changelog describes this release
if: inputs.mode == 'release'
shell: bash
run: node scripts/changelog-check.js --release "$(node -p "require('./api/package.json').version")"

- name: Check asset cache-busting
shell: bash
run: node scripts/bump-cache-busting.js --check
Expand Down
88 changes: 88 additions & 0 deletions .github/workflows/release-prep.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Release prep

on:
workflow_dispatch:
inputs:
version:
description: 'Version to release, without the leading v (for example 1.8.0)'
required: true
type: string

# Every write goes through the app token below. GITHUB_TOKEN needs no scope of
# its own, and a pull request it opened would start no checks.
permissions: {}

concurrency:
group: release
cancel-in-progress: false

jobs:
prep:
runs-on: ubuntu-latest
steps:
- name: Mint a token for the release app
id: app
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}

- name: Checkout default branch
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false

- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: '24'

# Dates the section, writes the compare link, bumps api/package.json and
# moves the demo pin in render.yaml.
- name: Prepare the release
env:
VERSION: ${{ inputs.version }}
run: node scripts/release-prep.js "$VERSION"

# The same gate the pull request will run, failing here instead of after
# a branch exists.
- name: Check the changelog
env:
VERSION: ${{ inputs.version }}
run: node scripts/changelog-check.js --release "$VERSION"

- name: Open the release pull request
env:
VERSION: ${{ inputs.version }}
BASE: ${{ github.event.repository.default_branch }}
REPO: ${{ github.repository }}
GH_TOKEN: ${{ steps.app.outputs.token }}
run: |
set -euo pipefail
BRANCH="release/v${VERSION}"
git config user.name 'stackyard-release[bot]'
git config user.email '${{ steps.app.outputs.app-slug }}[bot]@users.noreply.github.com'
git checkout -b "${BRANCH}"
git add CHANGELOG.md api/package.json render.yaml
git commit -m "Release ${VERSION}"
git push "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "HEAD:${BRANCH}"
gh pr create \
--base "${BASE}" \
--head "${BRANCH}" \
--title "Release: ${VERSION}" \
--label release \
--body "$(printf '%s\n' \
'**Summary**' \
"- Date the changelog section as ${VERSION} and add its compare link." \
"- Bump api/package.json to ${VERSION}." \
"- Pin the demo image to ${VERSION} in render.yaml." \
'' \
'**Testing**' \
'The changelog gate ran in release mode before this branch was pushed. Merging this tags the release and publishes the image.' \
'' \
'**Checklist**' \
'- [x] Stays within the project constraints: one container, no runtime dependencies, no build step (see CONTRIBUTING.md).' \
'- [ ] Tests added or updated for new logic, and the full suite, lint, and typechecks pass.' \
'- [ ] Docs updated if behavior or configuration changed.' \
'- [x] CHANGELOG.md updated under Unreleased if the change is user-visible.')"
72 changes: 72 additions & 0 deletions .github/workflows/release-tag.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Release tag

on:
push:
branches:
- main
paths:
- api/package.json

# The tag is pushed with the app token. A tag pushed with GITHUB_TOKEN starts no
# workflow run, so the image would never build.
permissions: {}

concurrency:
group: release
cancel-in-progress: false

jobs:
tag:
runs-on: ubuntu-latest
steps:
- name: Mint a token for the release app
id: app
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}

- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: '24'

# api/package.json changes for other reasons too. Only a version that the
# changelog already describes as released is a release.
- name: Decide whether this is a release
id: decide
env:
REPO: ${{ github.repository }}
run: |
set -euo pipefail
VERSION="$(node -p "require('./api/package.json').version")"
if git ls-remote --exit-code --tags "https://github.com/${REPO}.git" "v${VERSION}" >/dev/null 2>&1; then
echo "v${VERSION} is already tagged."
echo "release=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if ! node scripts/changelog-check.js --release "${VERSION}"; then
echo "::notice::api/package.json says ${VERSION}, but the tree is not release-ready. No tag."
echo "release=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "release=true" >> "$GITHUB_OUTPUT"

- name: Tag the release
if: steps.decide.outputs.release == 'true'
env:
VERSION: ${{ steps.decide.outputs.version }}
REPO: ${{ github.repository }}
GH_TOKEN: ${{ steps.app.outputs.token }}
run: |
set -euo pipefail
git config user.name 'stackyard-release[bot]'
git config user.email '${{ steps.app.outputs.app-slug }}[bot]@users.noreply.github.com'
git tag -a "v${VERSION}" -m "Release ${VERSION}"
git push "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "refs/tags/v${VERSION}"
57 changes: 57 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,60 @@ jobs:
repository: ${{ vars.DOCKERHUB_NAMESPACE || vars.DOCKERHUB_USERNAME }}/stackyard
readme-filepath: dockerhub-description.md
short-description: A self-hosted homelab dashboard you actually want to look at.

# A tag with an image and no release page has happened before. This runs after
# the image is published, so the page never describes something unpublished.
release-page:
needs: build-and-push
runs-on: ubuntu-latest
# Every write here is the app token's.
permissions: {}
steps:
# The app token, not GITHUB_TOKEN: a release published with the built-in
# token starts no workflow run, and the Community Applications template
# update listens for exactly that event.
- name: Mint a token for the release app
id: app
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}

- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: '24'

- name: Classify the tag
id: tag
run: node scripts/is-prerelease.js "$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"

# The notes are the changelog section, so the page and the file cannot
# describe the release differently.
- name: Publish the release page
env:
TAG: ${{ github.ref_name }}
PRERELEASE: ${{ steps.tag.outputs.prerelease }}
GH_TOKEN: ${{ steps.app.outputs.token }}
run: |
set -euo pipefail
if gh release view "$TAG" >/dev/null 2>&1; then
echo "$TAG already has a release page."
exit 0
fi
node scripts/release-notes.js "$TAG" > release-notes.md
if [ "$PRERELEASE" = 'true' ]; then
FLAG=--prerelease
else
FLAG=--latest
fi
gh release create "$TAG" \
--title "${TAG#v}" \
--notes-file release-notes.md \
--verify-tag \
"$FLAG"
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ in `.github/actions/checks/action.yml`, so the list here is the whole of it:

```
npm ci
node scripts/changelog-check.js
node scripts/bump-cache-busting.js --check
npm run paths:check
cd api && npm test
Expand All @@ -41,6 +42,12 @@ docker build -t stackyard:ci .
CodeQL also runs on every pull request. A finding it reports has to be resolved
before merge.

- **`changelog-check.js`** verifies the structure of `CHANGELOG.md`: the Keep a
Changelog header, the six section names, a real date on every released
version, descending versions, and a compare link per version. Structure fails
the check. Style problems in `[Unreleased]`, such as a rationale clause or a
three-sentence entry, print as warnings on the pull request and do not fail
it. Releasing is documented in [docs/releasing.md](docs/releasing.md).
- **`bump-cache-busting.js --check`** verifies that every `/css/` and `/js/`
reference carries a `?v=` stamp. The release build recomputes the hashes, so
write `?v=1` and never edit a stamp by hand.
Expand Down
Loading
Loading