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
73 changes: 73 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Runs every check on pull requests, pushes to main, and once a week. Each
# sample's workflow skips its jobs when the sample didn't change.
# "All checks passed" sums up the results and is the one check to require.
name: CI

on:
pull_request:
push:
branches: [main]
schedule:
- cron: "23 7 * * 1"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
workflows:
name: Workflows
uses: ./.github/workflows/workflows.yml

codeql:
name: CodeQL
uses: ./.github/workflows/codeql.yml
permissions:
actions: read
contents: read
security-events: write

foldersync:
name: FolderSync
uses: ./.github/workflows/foldersync.yml

all-passed:
name: All checks passed
if: always()
needs: [workflows, codeql, foldersync]
runs-on: ubuntu-latest
steps:
- env:
WORKFLOWS: ${{ needs.workflows.result }}
CODEQL: ${{ needs.codeql.result }}
FOLDERSYNC: ${{ needs.foldersync.result }}
run: |
icon() {
case "$1" in
success) echo "✅" ;;
skipped) echo "⏭️" ;;
*) echo "❌" ;;
esac
}
{
echo "## CI"
echo
echo "| Check | Result |"
echo "| --- | --- |"
echo "| Workflows (actionlint) | $(icon "$WORKFLOWS") $WORKFLOWS |"
echo "| CodeQL | $(icon "$CODEQL") $CODEQL |"
echo "| FolderSync | $(icon "$FOLDERSYNC") $FOLDERSYNC |"
} >> "$GITHUB_STEP_SUMMARY"
for result in "$WORKFLOWS" "$CODEQL" "$FOLDERSYNC"; do
case "$result" in
success | skipped) ;;
*)
echo "::error::A check didn't pass. See the table in this run's summary."
exit 1
;;
esac
done
9 changes: 3 additions & 6 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
# CodeQL code scanning for the samples' Go and TypeScript.
# CodeQL code scanning for the samples' Go and TypeScript. CI runs it, and
# it can be run by hand.
name: CodeQL

on:
pull_request:
push:
branches: [main]
schedule:
- cron: "23 7 * * 1"
workflow_call:
workflow_dispatch:

permissions:
Expand Down
94 changes: 94 additions & 0 deletions .github/workflows/foldersync-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Publishes a FolderSync release, run by hand from main. It checks and builds
# main with the FolderSync workflow, then creates the GitHub release
# foldersync-v<version> with the zip. Bump the version first; see
# CONTRIBUTING.md.
name: FolderSync release

on:
workflow_dispatch:
inputs:
e2e-passed:
description: The local live e2e tests passed on this commit
type: boolean
required: true
default: false

permissions:
contents: read

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

jobs:
prepare:
name: Check the release
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Require main and a local e2e pass
env:
REF: ${{ github.ref }}
E2E_PASSED: ${{ inputs.e2e-passed }}
run: |
if [ "$REF" != refs/heads/main ]; then
echo "::error::Release from main, not $REF."
exit 1
fi
if [ "$E2E_PASSED" != true ]; then
echo "::error::Run the local live e2e tests on this commit first, then check the box."
exit 1
fi
- uses: actions/checkout@v7
- id: version
run: |
version=$(jq -r .version foldersync/package.json)
# Like 0.1.0, or 0.1.0-preview.1 for a pre-release.
if ! echo "$version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$'; then
echo "::error::foldersync/package.json has an invalid version: $version"
exit 1
fi
tag="foldersync-v$version"
if git ls-remote --exit-code --tags origin "refs/tags/$tag" > /dev/null; then
echo "::error::$tag already exists. Bump the version in foldersync/package.json."
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "Releasing $tag"

build:
name: Check and build
needs: prepare
uses: ./.github/workflows/foldersync.yml

publish:
name: Publish the release
needs: [prepare, build]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v8
with:
name: FolderSync-mac-arm64
- env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
SHA: ${{ github.sha }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
shasum -a 256 FolderSync-mac-arm64.zip > SHA256SUMS
cat > notes.md << EOF
FolderSync $VERSION for macOS on Apple silicon, built from $SHA.

Download \`FolderSync-mac-arm64.zip\` and follow [Run FolderSync](https://github.com/$REPO/tree/foldersync-v$VERSION/foldersync#run-foldersync). The app isn't notarized; \`start.sh\` removes the flag macOS puts on downloaded files.
EOF
# A version with a hyphen, like 0.1.0-preview.1, is a pre-release.
prerelease=()
if [[ "$VERSION" == *-* ]]; then
prerelease=(--prerelease)
fi
gh release create "foldersync-v$VERSION" FolderSync-mac-arm64.zip SHA256SUMS \
--repo "$REPO" --target "$SHA" --title "FolderSync $VERSION" \
--notes-file notes.md "${prerelease[@]}"
105 changes: 94 additions & 11 deletions .github/workflows/foldersync.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,56 @@
# Checks FolderSync on every pull request and push to main. After a merge,
# or when run by hand, it also builds the macOS zip and keeps it as an artifact.
# Checks FolderSync when a pull request or push to main changes it. CI and the
# release workflow run it, and it can be run by hand. After a merge, by hand, or
# for a release, it also builds the macOS zip and keeps it as an artifact. Jobs
# skip, rather than not run, when FolderSync didn't change.
name: FolderSync

on:
pull_request:
push:
branches: [main]
workflow_call:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: foldersync-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
changes:
name: Find FolderSync changes
runs-on: ubuntu-latest
outputs:
foldersync: ${{ steps.diff.outputs.foldersync }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- id: diff
env:
EVENT: ${{ github.event_name }}
PR_BASE: ${{ github.event.pull_request.base.sha }}
PR_HEAD: ${{ github.event.pull_request.head.sha }}
BEFORE: ${{ github.event.before }}
AFTER: ${{ github.sha }}
run: |
case "$EVENT" in
pull_request) range="$PR_BASE...$PR_HEAD" ;;
push) range="$BEFORE..$AFTER" ;;
*) range="" ;;
esac
# Run by hand, weekly, for a release, or on a new branch: check everything.
if [ -z "$range" ] || [ "$BEFORE" = 0000000000000000000000000000000000000000 ]; then
changed=true
# -z lists paths unquoted, even with non-ASCII names.
elif git diff -z --name-only --no-renames "$range" |
grep -qzE '^(foldersync/|\.github/workflows/(foldersync|ci\.yml))'; then
changed=true
else
changed=false
fi
echo "foldersync=$changed" >> "$GITHUB_OUTPUT"
echo "FolderSync changed: $changed"

app:
name: App (lint, typecheck, test)
needs: changes
if: needs.changes.outputs.foldersync == 'true'
runs-on: macos-latest
defaults:
run:
Expand All @@ -37,6 +70,8 @@ jobs:

token-server:
name: Token server (vet, test, lint, vulnerabilities)
needs: changes
if: needs.changes.outputs.foldersync == 'true'
runs-on: ubuntu-latest
defaults:
run:
Expand All @@ -60,15 +95,17 @@ jobs:

scripts:
name: Shell scripts
needs: changes
if: needs.changes.outputs.foldersync == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: shellcheck foldersync/scripts/start.sh

package:
name: Package the macOS app
if: github.event_name != 'pull_request'
needs: [app, token-server]
if: github.event_name != 'pull_request' && github.event_name != 'schedule'
needs: [app, token-server, scripts]
runs-on: macos-latest
defaults:
run:
Expand All @@ -92,3 +129,49 @@ jobs:
name: FolderSync-mac-arm64
path: foldersync/out/FolderSync-mac-arm64.zip
retention-days: 30

result:
name: FolderSync result
if: always()
needs: [changes, app, token-server, scripts, package]
runs-on: ubuntu-latest
steps:
- env:
CHANGED: ${{ needs.changes.outputs.foldersync }}
CHANGES: ${{ needs.changes.result }}
APP: ${{ needs.app.result }}
TOKEN_SERVER: ${{ needs.token-server.result }}
SCRIPTS: ${{ needs.scripts.result }}
PACKAGE: ${{ needs.package.result }}
run: |
icon() {
case "$1" in
success) echo "✅" ;;
skipped) echo "⏭️" ;;
*) echo "❌" ;;
esac
}
{
echo "## FolderSync"
echo
if [ "$CHANGED" != true ]; then
echo "FolderSync didn't change, so its checks skipped."
echo
fi
echo "| Job | Result |"
echo "| --- | --- |"
echo "| Find FolderSync changes | $(icon "$CHANGES") $CHANGES |"
echo "| App (lint, typecheck, test) | $(icon "$APP") $APP |"
echo "| Token server (vet, test, lint, vulnerabilities) | $(icon "$TOKEN_SERVER") $TOKEN_SERVER |"
echo "| Shell scripts | $(icon "$SCRIPTS") $SCRIPTS |"
echo "| Package the macOS app | $(icon "$PACKAGE") $PACKAGE |"
} >> "$GITHUB_STEP_SUMMARY"
for result in "$CHANGES" "$APP" "$TOKEN_SERVER" "$SCRIPTS" "$PACKAGE"; do
case "$result" in
success | skipped) ;;
*)
echo "::error::A FolderSync job didn't pass."
exit 1
;;
esac
done
6 changes: 2 additions & 4 deletions .github/workflows/workflows.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
# Lints the GitHub Actions workflows.
# Lints the GitHub Actions workflows. CI runs it.
name: Workflows

on:
pull_request:
push:
branches: [main]
workflow_call:

permissions:
contents: read
Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ From `foldersync/`, for what you changed:
- `npm run format` formats the TypeScript and scripts with Prettier. CI checks it with `npm run format:check`.
- `cd tokenserver && go vet ./... && go test ./... && golangci-lint run` (golangci-lint 2.15 or later, built with Go 1.27 or later).
- `npm run build:tokenserver && npm run package`: builds `out/FolderSync-mac-arm64.zip` (macOS only).
- CI (`.github/workflows/foldersync.yml`) runs these checks, plus ShellCheck and `govulncheck`, on every pull request.
- CI (`.github/workflows/ci.yml`) calls `foldersync.yml`, which runs these checks plus ShellCheck and `govulncheck` when a pull request changes `foldersync/` or FolderSync's workflows; its jobs skip otherwise. A new sample needs its own workflow with the same first job, called from `ci.yml` and added to `all-passed`.
- Releases: `.github/workflows/foldersync-release.yml`, run by hand on `main`. See "Releasing FolderSync" in CONTRIBUTING.md.

## Local live e2e tests

Expand Down
13 changes: 12 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Each top-level folder is one sample, with its own README, dependencies, and test

- `foldersync/`: a desktop Electron app in TypeScript, and a token server in Go

`.github/workflows/ci.yml` runs on every pull request and calls each sample's own workflow, whose jobs skip unless that sample changed. Its **All checks passed** check sums up the results; open it in a pull request to see them.

## Working on FolderSync

You need Node.js 24 or later and Go 1.27.3 or later. The app runs on macOS on Apple silicon today.
Expand All @@ -28,12 +30,21 @@ npm test
(cd tokenserver && go vet ./... && go test ./... && golangci-lint run)
```

CI runs the same checks on every pull request.
CI runs the same checks on every pull request that changes FolderSync.

The local live e2e tests in `test/e2e/` run against real Dropbox with your own app's credentials; see "Local live e2e tests" in FolderSync's [README](foldersync/README.md#local-live-e2e-tests). We highly recommend running them before a pull request that changes sync, tokens, or agents. If you run them, include the result in your pull request. CI doesn't run them, so the repo never holds an app secret. Before a release, a maintainer runs them and includes the result in the pull request that changes the version.

The tests don't need a Dropbox account. They run against an in-memory Dropbox (`test/fakeDropbox.ts`) and a fake API (`test/fakeRpc.ts`). To build the app's zip file, run `npm run build:tokenserver && npm run package`. FolderSync's [README](foldersync/README.md#develop) lists the environment variables for development.

## Releasing FolderSync

Maintainers release FolderSync from `main`:

1. In a pull request, set the version in `foldersync/` with `npm version <version> --no-git-tag-version`, for example `0.1.0-preview.1`. Run the local live e2e tests and include the result.
2. After it merges, run the **FolderSync release** workflow on `main` from the Actions tab, and check the box that says the e2e tests passed.

The workflow runs FolderSync's checks, builds the zip, and creates the GitHub release `foldersync-v<version>` with the zip and its `SHA256SUMS`. A version with a hyphen, like `0.1.0-preview.1`, is published as a pre-release.

## Guidelines for sample code

- People copy sample code, so keep it small and easy to read. Comments say what the code does.
Expand Down
Loading