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
39 changes: 11 additions & 28 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# The same checks `omarchy plugin validate` runs: schema version, required
# fields, a well-formed id outside the reserved namespace, entry points
# that are safe relative paths and exist, and no symlinks in the tree.
# The same checks `omarchy plugin validate` runs; release.yml runs the
# same script before it tags and again before it publishes.
- name: Validate manifest.json
run: |
set -euo pipefail
jq -e '.schemaVersion == 1' manifest.json
for field in id name version kinds entryPoints; do
jq -e --arg f "$field" 'has($f)' manifest.json >/dev/null
done
jq -e '.id | test("^[A-Za-z0-9][A-Za-z0-9._-]*$")' manifest.json
jq -e '.id | (startswith("omarchy.") | not) and (contains("..") | not)' manifest.json
jq -e '(.kinds | type) == "array" and (.kinds | length) > 0' manifest.json
jq -e '(.entryPoints | type) == "object"' manifest.json
jq -e '.barWidget.defaultSection as $s | $s == null or (["left","center","right"] | index($s)) != null' manifest.json
jq -r '.entryPoints[]' manifest.json | while IFS= read -r ep; do
case "$ep" in
/*|*..*) echo "unsafe entry point: $ep"; exit 1 ;;
esac
test -f "$ep" || { echo "missing entry point: $ep"; exit 1; }
done
for kind in bar bar-widget menu overlay panel service; do
jq -e --arg k "$kind" '(.kinds | index($k)) == null' manifest.json >/dev/null && continue
key="$kind"; [[ $kind == bar-widget ]] && key=barWidget
jq -e --arg k "$key" '.entryPoints | has($k)' manifest.json >/dev/null \
|| { echo "kind '$kind' has no entryPoints.$key"; exit 1; }
done
link=$(find . -name .git -prune -o -type l -print -quit)
[[ -z $link ]] || { echo "symlink in plugin tree: $link"; exit 1; }
run: ./scripts/check-manifest.sh
# Bumps a disposable copy to a throwaway version and back, proving the
# release version rewrite is surgical and round-trips byte-for-byte.
- name: Set-version round trip
run: ./tests/test-set-version.sh
# Tags of the test's own making, so the release guard is proven without
# touching this repository's tags.
- name: Version bump guard
run: ./tests/test-check-version-bump.sh
- name: Data files parse
run: |
jq -e 'type == "array" and length > 0' cities.json >/dev/null
Expand Down
248 changes: 248 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
name: Release

# Two ways in:
# 1. workflow_dispatch (`cut`): sets the version in manifest.json on `main`,
# commits that if it changed anything, tags, and pushes. That push uses
# the default GITHUB_TOKEN, which GitHub deliberately does not let
# trigger further workflow runs -- so `publish` runs in this SAME run
# right after `cut`, rather than relying on the tag push to start a
# second run.
# 2. push of a `v*` tag (e.g. a tag pushed by a maintainer directly):
# `cut` is skipped and `publish` runs against that tag.
#
# There is no build step and no packaged asset. omarchy-pkgs builds the
# `elsewhen` package from the tag's source archive
# (https://github.com/omacom/elsewhen/archive/refs/tags/vX.Y.Z.tar.gz), so
# the tag itself is the release artifact; the GitHub release only marks it
# as published (and, for a `-` version, as a prerelease to be skipped).
on:
workflow_dispatch:
inputs:
version:
description: "Version to release, without a leading v (e.g. 1.2.3)"
required: true
type: string
push:
tags:
- "v*"

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

permissions:
contents: read

jobs:
cut:
# Only the manual entry point cuts a release; a direct tag push has
# already done the equivalent of this job by hand.
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: write
outputs:
tag: ${{ steps.tag.outputs.tag }}
steps:
- name: Require a release cut from main
if: github.ref != 'refs/heads/main'
env:
REF: ${{ github.ref }}
run: |
echo "::error::Release cuts must run from refs/heads/main (got $REF)"
exit 1

- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: main
fetch-depth: 0
fetch-tags: true

- name: Compute release tag
id: tag
env:
VERSION: ${{ inputs.version }}
run: |
tag="v$VERSION"
echo "tag=$tag" >> "$GITHUB_OUTPUT"

- name: Fail if the tag already exists
env:
TAG: ${{ steps.tag.outputs.tag }}
run: |
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
echo "::error::tag $TAG already exists locally"
exit 1
fi
if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then
echo "::error::tag $TAG already exists on origin"
exit 1
fi

- name: Require the version to move forward
# Above every existing v* tag and not below the manifest, so a
# mistyped version cannot become the "latest" release.
env:
VERSION: ${{ inputs.version }}
run: ./scripts/check-version-bump.sh "$VERSION"

- name: Set the release version
env:
VERSION: ${{ inputs.version }}
run: ./scripts/set-version.sh "$VERSION"

- name: Validate manifest.json
run: ./scripts/check-manifest.sh

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24

- name: Install Qt test runner and iso-codes
run: |
sudo apt-get update
sudo apt-get install --yes \
iso-codes \
qt6-declarative-dev-tools \
qml6-module-qtqml-workerscript \
qml6-module-qtquick \
qml6-module-qtquick-window \
qml6-module-qttest

- name: Run tests
run: ./tests/run --offline

- name: Commit and tag the release
env:
TAG: ${{ steps.tag.outputs.tag }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# main may already carry this version: the first release, 0.1.0,
# was written into manifest.json by hand before this workflow
# existed. Then there is nothing to commit and the tag goes on the
# existing HEAD; the branch push below is a no-op for that case.
if git diff --quiet; then
echo "main is already at $TAG; tagging HEAD as it is"
else
git diff --stat
git add -u
git commit -m "Release $TAG"
fi
git tag -a "$TAG" -m "Release $TAG"

- name: Push the release commit and tag
env:
TAG: ${{ steps.tag.outputs.tag }}
run: git push --atomic origin HEAD:refs/heads/main "refs/tags/$TAG"

publish:
needs: cut
# Runs whether `cut` just ran (workflow_dispatch) or was skipped (a
# direct tag push); does not run if `cut` ran and failed.
if: |
always() &&
(needs.cut.result == 'success' || needs.cut.result == 'skipped')
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: write
steps:
- name: Resolve the release tag
id: resolve
env:
CUT_TAG: ${{ needs.cut.outputs.tag }}
run: |
if [[ -n "$CUT_TAG" ]]; then
tag="$CUT_TAG"
else
tag="${GITHUB_REF#refs/tags/}"
fi
echo "tag=$tag" >> "$GITHUB_OUTPUT"

- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ steps.resolve.outputs.tag }}
fetch-depth: 0

- name: Require the tag to be on main
# The dispatch path only ever tags a commit `cut` just pushed to
# main, so this always holds there; it's the tag-push path (a
# maintainer pushing a `v*` tag directly, bypassing `cut`) that this
# guards: publishing must not happen from a commit main hasn't seen.
env:
TAG: ${{ steps.resolve.outputs.tag }}
run: |
git fetch origin main
if ! git merge-base --is-ancestor HEAD origin/main; then
echo "::error::tag $TAG is not an ancestor of origin/main; refusing to publish"
exit 1
fi

- name: Verify the tag matches the manifest version
env:
TAG: ${{ steps.resolve.outputs.tag }}
run: |
manifest_version=$(jq -r '.version' manifest.json)
[[ $TAG == "v$manifest_version" ]] || {
echo "::error::tag $TAG does not match manifest.json version $manifest_version"
exit 1
}

- name: Validate manifest.json
run: ./scripts/check-manifest.sh

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24

- name: Install Qt test runner and iso-codes
run: |
sudo apt-get update
sudo apt-get install --yes \
iso-codes \
qt6-declarative-dev-tools \
qml6-module-qtqml-workerscript \
qml6-module-qtquick \
qml6-module-qtquick-window \
qml6-module-qttest

- name: Run tests
run: ./tests/run --offline

- name: Determine the prerelease flag
id: prerelease
env:
TAG: ${{ steps.resolve.outputs.tag }}
run: |
if [[ $TAG == *-* ]]; then
echo "flag=--prerelease" >> "$GITHUB_OUTPUT"
else
echo "flag=" >> "$GITHUB_OUTPUT"
fi

- name: Fail if a release for this tag already exists
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.resolve.outputs.tag }}
run: |
if gh release view "$TAG" >/dev/null 2>&1; then
echo "::error::a release for $TAG already exists"
exit 1
fi

- name: Create the GitHub release
# No assets: omarchy-pkgs consumes the tag's source archive, so the
# tag is the artifact and the release is the signal that it's ready.
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.resolve.outputs.tag }}
PRERELEASE_FLAG: ${{ steps.prerelease.outputs.flag }}
run: |
# shellcheck disable=SC2086
gh release create "$TAG" \
--verify-tag \
--title "$TAG" \
--generate-notes \
$PRERELEASE_FLAG
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,23 @@ clocks, one row per city, with a spinnable globe behind it.

## Installing

Newer Omarchy installs carry Elsewhen as the `elsewhen` package: installed
by default, kept current by `omarchy update`, and living in
`/usr/share/omarchy/plugins/omacom.elsewhen`. There is nothing to add.

On an Omarchy from before the package, or to hack on it, the plugin can be
added from this repository instead:

```bash
omarchy plugin add https://github.com/omacom/elsewhen.git --enable
```

That clones this repository into `~/.config/omarchy/plugins/omacom.elsewhen`
and places the widget in the bar's right section. Without `--enable` it asks
first. To take it out again:
first. While the package is installed the shell prefers the packaged copy: a
checkout under `~/.config/omarchy/plugins` with the same id is rejected with
a warning, so it only takes effect where the package is absent. To take it
out again:

```bash
omarchy plugin remove omacom.elsewhen
Expand Down Expand Up @@ -1261,3 +1271,37 @@ omarchy-shell omacom.elsewhen add America/New_York Miami
omarchy-shell omacom.elsewhen remove America/New_York
omarchy-shell omacom.elsewhen refresh # re-probe offsets
```

## Releasing

Releases are cut from GitHub: Actions, Release, Run workflow, with `version`
set to the new version and no leading `v` (`0.2.0`, or `0.2.0-rc.1` for a
prerelease). The `cut` job runs on `main` and refuses any other branch, a
tag that already exists, and a version that is not above every existing tag
and at least the manifest's own, so a mistyped version cannot become the
latest release. It writes the version into `manifest.json` with
`scripts/set-version.sh` - the only place a version is recorded - runs
`scripts/check-manifest.sh` and
`tests/run --offline`, commits `Release vX.Y.Z` as `github-actions[bot]` if
that changed anything, tags the commit `vX.Y.Z`, and pushes both. When `main`
already carries the version, as it did for `0.1.0`, there is nothing to
commit and the tag goes on the existing head.

The `publish` job then runs in the same workflow run, because a push made
with the workflow's own token does not start another one. It checks out the
tag, requires its commit to be on `main` and the tag to equal `v` plus the
manifest's version, runs the same checks again, and creates the GitHub
release with generated notes. A version with a `-` in it is marked a
prerelease, which `omarchy-pkgs` skips when it looks for the latest release;
that is also why `set-version.sh` refuses a `.`-introduced suffix like
`0.2.0.rc1`, which would slip past. Nothing is attached to the release:
`omarchy-pkgs` builds the `elsewhen` package from the tag's source archive,
so the tag is the release.

Pushing a `vX.Y.Z` tag by hand skips `cut` and publishes that tag the same
way. If `cut` succeeds and `publish` fails, re-run the failed job of that same
run from the Actions page; the commit and tag are already on `main`, so
dispatching the workflow again with the same version stops at the
tag-already-exists check, and bumping past it would leave the first version
unpublished. The repository has to be public before `omarchy-pkgs` can fetch
the archive at all.
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"schemaVersion": 1,
"id": "omacom.elsewhen",
"name": "Elsewhen",
"version": "1.0.0",
"version": "0.1.0",
"author": "Jason Fried",
"license": "MIT",
"description": "A world clock for the Omarchy shell: the time in several cities at once, with a spinnable globe.",
Expand Down
Loading
Loading