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
29 changes: 29 additions & 0 deletions .github/RELEASE-POLICY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Changes and releases

All code changes go through pull requests. Draft PRs skip expensive validation;
mark the PR ready to run relevant checks on the current revision. Superseded PR
runs cancel. Do not bypass required checks.

## Prepare a release

Update release notes and applicable version examples, and write the tag (for example `v1.2.3`) to `.github/release-request`. Its merge publishes CLI and desktop assets from that exact commit.

Use a local authenticated `gh` session, directly or through an LLM, to create a
branch named `release/<version>`, make the version and release-note changes, and open a draft PR with
`gh pr create --draft`. Mark it ready with `gh pr ready` when preparation is
complete. Wait for required checks before merging.

"Prepare a release" stops at the PR. A human merge, or an explicit instruction
to an LLM to release, authorizes merging that PR and publishing. Never merge a
release or dependency PR unattended. Never create a new version merely to retry
a failed publisher: inspect the failed run and its published artifacts first.

The local gh path needs no new secrets. A future Prepare release Action must
have permission to create PRs; token-created PRs need a human ready event (or
a separately authorized token) to trigger normal PR CI. No such token is assumed.

Routine dependency updates share one weekly multi-ecosystem group. Security updates
remain eligible immediately and are not held for the routine weekly batch.

Tap publishing tokens need both Contents and Pull requests write permission on
the destination tap. Tap updates require review and merge after binary publication.
41 changes: 17 additions & 24 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
version: 2
updates:
- package-ecosystem: gomod
directory: "/"
- package-ecosystem: gomod
directory: /
patterns:
- '*'
multi-ecosystem-group: routine
- package-ecosystem: gomod
directory: /desktop
patterns:
- '*'
multi-ecosystem-group: routine
- package-ecosystem: github-actions
directory: /
patterns:
- '*'
multi-ecosystem-group: routine
multi-ecosystem-groups:
routine:
schedule:
interval: weekly
groups:
go-deps:
patterns: ["*"]
# desktop/ is a SEPARATE module. Dependabot v2 does not auto-discover nested
# modules, so without this entry it never received an update PR — and no root
# check reaches it either (govulncheck/vet/golangci/CodeQL all run from the
# root, where `go list ./...` returns zero desktop packages). That gap is why
# x/net, x/crypto and x/text there have needed hand-written catch-up bumps.
- package-ecosystem: gomod
directory: "/desktop"
schedule:
interval: weekly
groups:
desktop-go-deps:
patterns: ["*"]
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
groups:
actions:
patterns: ["*"]
79 changes: 72 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,61 @@
name: CI

on:
push:
branches: [main]
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

permissions:
contents: read
pull-requests: read

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
changes:
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
code: ${{ steps.filter.outputs.code }}
steps:
- id: filter
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD: ${{ github.event.pull_request.head.sha }}
PR_BASE: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
if [ "$GITHUB_EVENT_NAME" != pull_request ]; then
echo 'code=true' >> "$GITHUB_OUTPUT"
exit 0
fi
endpoint="repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER"
gh api "$endpoint" > "$RUNNER_TEMP/pr-before.json"
if ! jq -e --arg head "$PR_HEAD" --arg base "$PR_BASE" '.head.sha == $head and .base.sha == $base' "$RUNNER_TEMP/pr-before.json" >/dev/null; then
echo 'code=true' >> "$GITHUB_OUTPUT"
exit 0
fi
total=$(jq -er '.changed_files' "$RUNNER_TEMP/pr-before.json")
gh api --paginate --slurp "$endpoint/files" > "$RUNNER_TEMP/changed-files.json"
gh api "$endpoint" > "$RUNNER_TEMP/pr-after.json"
if ! jq -e --arg head "$PR_HEAD" --arg base "$PR_BASE" '.head.sha == $head and .base.sha == $base' "$RUNNER_TEMP/pr-after.json" >/dev/null; then
echo 'code=true' >> "$GITHUB_OUTPUT"
exit 0
fi
value=$(jq --argjson total "$total" 'add | if length == 0 or length != $total then true else any(.[]; ([.filename, (.previous_filename // .filename)] | any(.[]; test("^(docs/.*\\.md|[^/]+\\.md|LICENSE)$") | not))) end' "$RUNNER_TEMP/changed-files.json")
echo "code=$value" >> "$GITHUB_OUTPUT"

test:
needs: changes
if: needs.changes.outputs.code == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod
cache: true
Expand All @@ -37,21 +82,24 @@ jobs:
run: cd desktop && go build ./...

web:
needs: changes
if: needs.changes.outputs.code == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
# Run on Linux (case-sensitive) AND macOS (case-insensitive). The macOS leg
# catches case-only filename collisions — e.g. a Foo.svelte component next to
# a foo.svelte.ts runes module — which resolve fine on Linux but break
# svelte-check on macOS/Windows dev machines and release runners.
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
os: [macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 20
defaults:
run:
working-directory: internal/web/frontend
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
cache: npm
Expand All @@ -61,3 +109,20 @@ jobs:
run: npm run check
- name: vitest
run: npm test

result:
name: ${{ github.workflow }} result
if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
needs: [changes, test, web]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- env:
RESULTS: ${{ toJSON(needs) }}
run: |
python3 - <<'PYTHON'
import json, os
results = json.loads(os.environ['RESULTS'])
assert results['changes']['result'] == 'success', 'Change detection did not succeed'
assert all(job['result'] in ('success', 'skipped') for job in results.values()), results
PYTHON
17 changes: 11 additions & 6 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
name: CodeQL

on:
push:
branches: [main]
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches: [main]
schedule:
- cron: "0 9 * * 1" # weekly

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
analyze:
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
security-events: write
actions: read
contents: read
steps:
- uses: actions/checkout@v7
- uses: github/codeql-action/init@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4
with:
languages: go
- uses: github/codeql-action/autobuild@v4
- uses: github/codeql-action/analyze@v4
- uses: github/codeql-action/autobuild@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4
- uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4
with:
category: "/language:go"
19 changes: 14 additions & 5 deletions .github/workflows/govulncheck.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
name: Vulncheck

on:
push:
branches: [main]
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
schedule:
- cron: "0 9 * * 1" # weekly, to catch newly-disclosed CVEs in deps

permissions:
contents: read
pull-requests: read

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
govulncheck:
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod
go-version: '1.26.x'
# Install + run govulncheck directly instead of golang/govulncheck-action,
# whose internal git checkout was failing in CI with "unable to access … 400"
# (git exit 128) on every run. This direct invocation is what we run locally
Expand Down
72 changes: 67 additions & 5 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,85 @@
name: Lint

on:
push:
branches: [main]
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

permissions:
contents: read
pull-requests: read

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
changes:
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
code: ${{ steps.filter.outputs.code }}
steps:
- id: filter
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD: ${{ github.event.pull_request.head.sha }}
PR_BASE: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
if [ "$GITHUB_EVENT_NAME" != pull_request ]; then
echo 'code=true' >> "$GITHUB_OUTPUT"
exit 0
fi
endpoint="repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER"
gh api "$endpoint" > "$RUNNER_TEMP/pr-before.json"
if ! jq -e --arg head "$PR_HEAD" --arg base "$PR_BASE" '.head.sha == $head and .base.sha == $base' "$RUNNER_TEMP/pr-before.json" >/dev/null; then
echo 'code=true' >> "$GITHUB_OUTPUT"
exit 0
fi
total=$(jq -er '.changed_files' "$RUNNER_TEMP/pr-before.json")
gh api --paginate --slurp "$endpoint/files" > "$RUNNER_TEMP/changed-files.json"
gh api "$endpoint" > "$RUNNER_TEMP/pr-after.json"
if ! jq -e --arg head "$PR_HEAD" --arg base "$PR_BASE" '.head.sha == $head and .base.sha == $base' "$RUNNER_TEMP/pr-after.json" >/dev/null; then
echo 'code=true' >> "$GITHUB_OUTPUT"
exit 0
fi
value=$(jq --argjson total "$total" 'add | if length == 0 or length != $total then true else any(.[]; ([.filename, (.previous_filename // .filename)] | any(.[]; test("^(docs/.*\\.md|[^/]+\\.md|LICENSE)$") | not))) end' "$RUNNER_TEMP/changed-files.json")
echo "code=$value" >> "$GITHUB_OUTPUT"

golangci:
needs: changes
if: needs.changes.outputs.code == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod
cache: true
- uses: golangci/golangci-lint-action@v9
- uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9
with:
version: v2.12.2
# Don't reuse the action's analysis cache: a stale cache was pinning
# SA5011 false positives that a fresh run (and local v2.12.2) doesn't
# report. Re-analyze each run for correct, reproducible results.
skip-cache: true

result:
name: ${{ github.workflow }} result
if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
needs: [changes, golangci]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- env:
RESULTS: ${{ toJSON(needs) }}
run: |
python3 - <<'PYTHON'
import json, os
results = json.loads(os.environ['RESULTS'])
assert results['changes']['result'] == 'success', 'Change detection did not succeed'
assert all(job['result'] in ('success', 'skipped') for job in results.values()), results
PYTHON
9 changes: 5 additions & 4 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ concurrency:
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 20
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v7
- uses: actions/configure-pages@v6
- uses: actions/upload-pages-artifact@v5
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
with:
path: site
- id: deployment
uses: actions/deploy-pages@v5
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5
Loading
Loading