Skip to content

Commit b4a754d

Browse files
miaulalalaclaude
andcommitted
feat: add on-demand screenshot workflow for documentation
Adds a Cypress spec and GitHub Actions workflow to capture deterministic screenshots of the Activity app for use in the Nextcloud documentation. - Triggered manually via workflow_dispatch with a configurable server branch - Captures user views (activity stream filters, sidebar, personal settings) and admin views (notification toggle, default settings) at 1280x720 - Uploads screenshots as separate user/ and admin/ artifacts (5-day retention) - Optionally opens a PR against nextcloud/documentation via COMMAND_BOT_PAT - PR title includes branch prefix for stable branches (e.g. [stable30]) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Anna Larch <anna@nextcloud.com>
1 parent 508f939 commit b4a754d

File tree

2 files changed

+316
-0
lines changed

2 files changed

+316
-0
lines changed

.github/workflows/screenshots.yml

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
2+
# SPDX-License-Identifier: MIT
3+
4+
name: Documentation Screenshots
5+
6+
on:
7+
workflow_dispatch:
8+
inputs:
9+
server-branch:
10+
description: 'Server branch to screenshot against (e.g. master, stable30)'
11+
required: false
12+
default: 'master'
13+
open-docs-pr:
14+
description: 'Open a PR against nextcloud/documentation with the screenshots'
15+
type: boolean
16+
required: false
17+
default: true
18+
19+
permissions:
20+
contents: read
21+
22+
env:
23+
APP_NAME: ${{ github.event.repository.name }}
24+
BRANCH: ${{ inputs.server-branch || 'master' }}
25+
26+
jobs:
27+
screenshots:
28+
runs-on: ubuntu-latest
29+
name: Capture documentation screenshots
30+
31+
steps:
32+
- name: Checkout app
33+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
34+
with:
35+
persist-credentials: false
36+
37+
- name: Check composer.json
38+
id: check_composer
39+
uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v3.1.0
40+
with:
41+
files: 'composer.json'
42+
43+
- name: Install composer dependencies
44+
if: steps.check_composer.outputs.files_exists == 'true'
45+
run: composer install --no-dev
46+
47+
- name: Read package.json node and npm engines version
48+
uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3
49+
id: versions
50+
with:
51+
fallbackNode: '^24'
52+
fallbackNpm: '^11.3'
53+
54+
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
55+
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
56+
with:
57+
node-version: ${{ steps.versions.outputs.nodeVersion }}
58+
59+
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
60+
run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}'
61+
62+
- name: Install node dependencies & build app
63+
run: |
64+
npm ci
65+
TESTING=true npm run build --if-present
66+
67+
- name: Run screenshot specs
68+
uses: cypress-io/github-action@783cb3f07983868532cabaedaa1e6c00ff4786a8 # v7.1.9
69+
with:
70+
component: false
71+
spec: cypress/e2e/screenshots.cy.ts
72+
env:
73+
CYPRESS_BRANCH: ${{ env.BRANCH }}
74+
TESTING: true
75+
76+
- name: Upload user screenshots
77+
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
78+
if: always()
79+
with:
80+
name: screenshots-user
81+
path: cypress/snapshots/actual/screenshots.cy.ts/user/
82+
if-no-files-found: warn
83+
retention-days: 5
84+
85+
- name: Upload admin screenshots
86+
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
87+
if: always()
88+
with:
89+
name: screenshots-admin
90+
path: cypress/snapshots/actual/screenshots.cy.ts/admin/
91+
if-no-files-found: warn
92+
retention-days: 5
93+
94+
# Opens a PR against nextcloud/documentation with updated screenshots.
95+
# Uses the org-wide COMMAND_BOT_PAT secret.
96+
# The step is skipped gracefully if the secret is not configured.
97+
- name: Check if docs PR should be created
98+
id: docs-pr-check
99+
env:
100+
COMMAND_BOT_PAT: ${{ secrets.COMMAND_BOT_PAT }}
101+
OPEN_DOCS_PR: ${{ inputs.open-docs-pr }}
102+
run: |
103+
echo "::group::Debug info"
104+
echo "inputs.open-docs-pr = '${OPEN_DOCS_PR}'"
105+
echo "COMMAND_BOT_PAT is set = $([ -n "$COMMAND_BOT_PAT" ] && echo 'yes' || echo 'no')"
106+
echo "event_name = ${{ github.event_name }}"
107+
echo "::endgroup::"
108+
109+
if [ -z "$COMMAND_BOT_PAT" ]; then
110+
echo "COMMAND_BOT_PAT is not set, skipping docs PR."
111+
echo "should_run=false" >> "$GITHUB_OUTPUT"
112+
elif [ "$OPEN_DOCS_PR" = "false" ]; then
113+
echo "open-docs-pr is false, skipping docs PR."
114+
echo "should_run=false" >> "$GITHUB_OUTPUT"
115+
else
116+
echo "Will attempt to create docs PR."
117+
echo "should_run=true" >> "$GITHUB_OUTPUT"
118+
fi
119+
120+
- name: Open PR against nextcloud/documentation
121+
if: steps.docs-pr-check.outputs.should_run == 'true'
122+
env:
123+
COMMAND_BOT_PAT: ${{ secrets.COMMAND_BOT_PAT }}
124+
run: |
125+
set -e
126+
DOCS_BRANCH="chore/activity-screenshots-$(date +%Y%m%d-%H%M%S)"
127+
SCREENSHOT_DIR="cypress/snapshots/actual/screenshots.cy.ts"
128+
129+
git clone --depth 1 "https://x-access-token:${COMMAND_BOT_PAT}@github.com/nextcloud/documentation.git" /tmp/documentation
130+
cd /tmp/documentation
131+
132+
git config user.name "nextcloud-command"
133+
git config user.email "nextcloud-command@users.noreply.github.com"
134+
git checkout -b "$DOCS_BRANCH"
135+
136+
# Copy user screenshots
137+
if [ -d "$GITHUB_WORKSPACE/$SCREENSHOT_DIR/user" ]; then
138+
cp "$GITHUB_WORKSPACE/$SCREENSHOT_DIR/user/"*.png user_manual/images/
139+
echo "Copied user screenshots"
140+
else
141+
echo "WARNING: No user screenshot directory at $SCREENSHOT_DIR/user"
142+
fi
143+
144+
# Copy admin screenshots
145+
if [ -d "$GITHUB_WORKSPACE/$SCREENSHOT_DIR/admin" ]; then
146+
cp "$GITHUB_WORKSPACE/$SCREENSHOT_DIR/admin/"*.png admin_manual/images/
147+
echo "Copied admin screenshots"
148+
else
149+
echo "WARNING: No admin screenshot directory at $SCREENSHOT_DIR/admin"
150+
fi
151+
152+
# Check if there are changes to commit
153+
if git diff --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]; then
154+
echo "No screenshot changes detected, skipping PR."
155+
exit 0
156+
fi
157+
158+
# Build PR title with branch prefix for stable branches
159+
if [ "$BRANCH" != "master" ]; then
160+
PR_TITLE="[$BRANCH] chore: update activity app screenshots"
161+
else
162+
PR_TITLE="chore: update activity app screenshots"
163+
fi
164+
165+
git add user_manual/images/activity-*.png admin_manual/images/activity-*.png
166+
git diff --cached --stat
167+
git commit -s -m "$PR_TITLE"
168+
git push origin "$DOCS_BRANCH"
169+
echo "Pushed branch $DOCS_BRANCH"
170+
171+
GH_TOKEN="${COMMAND_BOT_PAT}" gh pr create \
172+
--repo nextcloud/documentation \
173+
--base master \
174+
--head "$DOCS_BRANCH" \
175+
--title "$PR_TITLE" \
176+
--body "$(cat <<EOF
177+
## Summary
178+
- Automated screenshot update from the [Activity app](https://github.com/nextcloud/activity) CI
179+
- Screenshots captured against server branch \`${BRANCH}\`
180+
181+
### User manual images
182+
$(cd "$GITHUB_WORKSPACE" && ls $SCREENSHOT_DIR/user/*.png 2>/dev/null | xargs -I{} basename {} | sed 's/^/- /' || echo "- (none)")
183+
184+
### Admin manual images
185+
$(cd "$GITHUB_WORKSPACE" && ls $SCREENSHOT_DIR/admin/*.png 2>/dev/null | xargs -I{} basename {} | sed 's/^/- /' || echo "- (none)")
186+
187+
---
188+
Generated automatically by the Activity app [screenshot workflow](https://github.com/nextcloud/activity/actions/workflows/screenshots.yml).
189+
EOF
190+
)"
191+
192+
echo "PR created successfully"
193+
194+
- name: Extract NC logs
195+
if: failure()
196+
run: docker logs nextcloud-cypress-tests-${{ env.APP_NAME }} > nextcloud.log 2>&1
197+
198+
- name: Upload NC logs
199+
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
200+
if: failure()
201+
with:
202+
name: nc-logs
203+
path: nextcloud.log

cypress/e2e/screenshots.cy.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { User } from '@nextcloud/cypress'
7+
import { createFolder, getFileListRow, renameFile } from './filesUtils.ts'
8+
import { addTag, showActivityTab } from './sidebarUtils.ts'
9+
10+
/**
11+
* Take a named screenshot with consistent settings for documentation.
12+
* Screenshots are organized into subdirectories:
13+
* cypress/snapshots/actual/user/ — user-facing views
14+
* cypress/snapshots/actual/admin/ — admin settings
15+
*/
16+
function docScreenshot(name: string, options: Partial<Cypress.ScreenshotOptions> = {}) {
17+
// Let animations, loaders and toasts settle
18+
cy.wait(500)
19+
cy.screenshot(name, {
20+
capture: 'viewport',
21+
overwrite: true,
22+
...options,
23+
})
24+
}
25+
26+
describe('Documentation screenshots — user views', { testIsolation: false }, () => {
27+
let user: User
28+
29+
before(() => {
30+
cy.createRandomUser()
31+
.then((_user) => {
32+
user = _user
33+
cy.login(user)
34+
})
35+
36+
// Seed activity: generate several types of events so the stream looks realistic
37+
cy.visit('/apps/files')
38+
getFileListRow('welcome.txt').should('be.visible')
39+
40+
createFolder('Project notes')
41+
renameFile('welcome.txt', 'readme.txt')
42+
43+
// Revisit to let the file list settle after rename
44+
cy.visit('/apps/files')
45+
getFileListRow('readme.txt').should('be.visible')
46+
47+
addTag('readme.txt', 'important')
48+
})
49+
50+
describe('Activity stream', () => {
51+
52+
it('Activity app — main stream (all filter)', () => {
53+
cy.visit('/apps/activity')
54+
cy.get('.activity-entry').should('have.length.at.least', 3)
55+
docScreenshot('user/activity-stream-all')
56+
})
57+
58+
it('Activity app — "by you" filter', () => {
59+
cy.visit('/apps/activity')
60+
cy.get('.activity-entry').should('have.length.at.least', 1)
61+
cy.get('[data-navigation="self"]').click()
62+
cy.get('.activity-entry').should('have.length.at.least', 1)
63+
docScreenshot('user/activity-stream-self')
64+
})
65+
66+
it('Activity app — "by others" filter', () => {
67+
cy.visit('/apps/activity')
68+
cy.get('.activity-entry').should('have.length.at.least', 1)
69+
cy.get('[data-navigation="by"]').click()
70+
// May be empty — that is expected for a single-user setup
71+
docScreenshot('user/activity-stream-by-others')
72+
})
73+
74+
it('Activity app — file changes filter', () => {
75+
cy.visit('/apps/activity')
76+
cy.get('.activity-entry').should('have.length.at.least', 1)
77+
cy.get('[data-navigation="files"]').click()
78+
cy.get('.activity-entry').should('have.length.at.least', 1)
79+
docScreenshot('user/activity-stream-file-changes')
80+
})
81+
})
82+
83+
describe('Files sidebar activity tab', () => {
84+
it('Sidebar — activity tab for a file', () => {
85+
cy.visit('/apps/files')
86+
getFileListRow('readme.txt').should('be.visible')
87+
showActivityTab('readme.txt')
88+
cy.get('.activity-entry').should('have.length.at.least', 1)
89+
docScreenshot('user/activity-sidebar')
90+
})
91+
})
92+
93+
describe('Personal notification settings', () => {
94+
it('Personal settings — notification preferences', () => {
95+
cy.visit('/settings/user/notifications')
96+
cy.get("#app-content input[type='checkbox']").should('have.length.at.least', 1)
97+
docScreenshot('user/activity-settings-personal')
98+
})
99+
})
100+
})
101+
102+
describe('Documentation screenshots — admin views', () => {
103+
104+
it('Admin settings — notification toggle and default settings', () => {
105+
// Log in as the default admin user provided by the Docker container
106+
const admin = new User('admin', 'admin')
107+
cy.login(admin)
108+
cy.visit('/settings/admin/activity')
109+
cy.get('#activity-admin-settings').should('be.visible')
110+
cy.get('#activity-default-settings').should('be.visible')
111+
docScreenshot('admin/activity-settings-admin')
112+
})
113+
})

0 commit comments

Comments
 (0)