feat(app): wire releases and the blob store, S3 configuration, MinIO compose profile and docs
- commit
- d50b7cd4137471821f40569c63a0e6468b52981c
- author
- brice · · 2026-10-21 15:41 UTC
- committer
- brice · · 2026-10-21 17:53 UTC
- parent
- 0933a48
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
47 files · +1172 -198
M .hexcheck.yaml +5 -0
| @@ -68,6 +68,11 @@ mocking: | |||
| 68 | 68 | generatedMockNamePatterns: | |
| 69 | 69 | - '{{Interface}}' | |
| 70 | 70 | allow: | |
| 71 | + | # Secret-source exclusivity and file decoding are configuration validation, | |
| 72 | + | # not domain policy; config is intentionally a composition-root helper. | |
| 73 | + | - rule: suspicious-business-logic-in-adapter | |
| 74 | + | path: internal/config/config.go | |
| 75 | + | reason: validates explicit environment and secret-file sources at startup | |
| 71 | 76 | # clientlimit is a shared infrastructure library, not a sibling adapter: the | |
| 72 | 77 | # HTTP adapters that rate-limit clients import it by design. | |
| 73 | 78 | - rule: no-adapter-to-adapter-imports | |
| @@ -68,6 +68,11 @@ mocking: | |||||
| 68 | generatedMockNamePatterns: | 68 | generatedMockNamePatterns: | ||
| 69 | - '{{Interface}}' | 69 | - '{{Interface}}' | ||
| 70 | allow: | 70 | allow: | ||
| 71 | + | # Secret-source exclusivity and file decoding are configuration validation, | |||
| 72 | + | # not domain policy; config is intentionally a composition-root helper. | |||
| 73 | + | - rule: suspicious-business-logic-in-adapter | |||
| 74 | + | path: internal/config/config.go | |||
| 75 | + | reason: validates explicit environment and secret-file sources at startup | |||
| 71 | # clientlimit is a shared infrastructure library, not a sibling adapter: the | 76 | # clientlimit is a shared infrastructure library, not a sibling adapter: the | ||
| 72 | # HTTP adapters that rate-limit clients import it by design. | 77 | # HTTP adapters that rate-limit clients import it by design. | ||
| 73 | - rule: no-adapter-to-adapter-imports | 78 | - rule: no-adapter-to-adapter-imports | ||
M README.md +1 -0
| @@ -18,6 +18,7 @@ | |||
| 18 | 18 | - **Isolated runner**: [ermite-runner](https://github.com/bnema/ermite-runner) runs each job in a Firecracker microVM when KVM is available, else in a container | |
| 19 | 19 | - **CLI-first**: repositories, merge requests, reviews and CI all work from the `ermite` CLI, for you and for AI agents | |
| 20 | 20 | - **Safe for agents**: an agent token can only push its own branches; you review and merge | |
| 21 | + | - **[Releases](docs/releases.md)** with a GitHub-compatible API, CLI and web downloads; assets and CI artifacts on [disk or S3-compatible storage](docs/storage.md) | |
| 21 | 22 | - Git over HTTPS, web interface with live updates | |
| 22 | 23 | ||
| 23 | 24 | Status: early development. | |
| @@ -18,6 +18,7 @@ | |||||
| 18 | - **Isolated runner**: [ermite-runner](https://github.com/bnema/ermite-runner) runs each job in a Firecracker microVM when KVM is available, else in a container | 18 | - **Isolated runner**: [ermite-runner](https://github.com/bnema/ermite-runner) runs each job in a Firecracker microVM when KVM is available, else in a container | ||
| 19 | - **CLI-first**: repositories, merge requests, reviews and CI all work from the `ermite` CLI, for you and for AI agents | 19 | - **CLI-first**: repositories, merge requests, reviews and CI all work from the `ermite` CLI, for you and for AI agents | ||
| 20 | - **Safe for agents**: an agent token can only push its own branches; you review and merge | 20 | - **Safe for agents**: an agent token can only push its own branches; you review and merge | ||
| 21 | + | - **[Releases](docs/releases.md)** with a GitHub-compatible API, CLI and web downloads; assets and CI artifacts on [disk or S3-compatible storage](docs/storage.md) | |||
| 21 | - Git over HTTPS, web interface with live updates | 22 | - Git over HTTPS, web interface with live updates | ||
| 22 | 23 | ||||
| 23 | Status: early development. | 24 | Status: early development. | ||
M compose.yaml +39 -0
| @@ -24,13 +24,52 @@ services: | |||
| 24 | 24 | ERMITE_ADMIN_TOKEN: dev-only-admin-token-do-not-use-0123456789 | |
| 25 | 25 | ERMITE_LOG_FORMAT: console | |
| 26 | 26 | ERMITE_LOG_LEVEL: debug | |
| 27 | + | # Empty bucket keeps the disk backend, even with the s3 profile active. | |
| 28 | + | ERMITE_S3_BUCKET: ${ERMITE_S3_BUCKET:-} | |
| 29 | + | ERMITE_S3_ENDPOINT: ${ERMITE_S3_ENDPOINT:-} | |
| 30 | + | ERMITE_S3_REGION: ${ERMITE_S3_REGION:-} | |
| 31 | + | ERMITE_S3_ACCESS_KEY_ID: ${ERMITE_S3_ACCESS_KEY_ID:-} | |
| 32 | + | ERMITE_S3_SECRET_ACCESS_KEY: ${ERMITE_S3_SECRET_ACCESS_KEY:-} | |
| 33 | + | ERMITE_S3_FORCE_PATH_STYLE: ${ERMITE_S3_FORCE_PATH_STYLE:-0} | |
| 34 | + | ERMITE_S3_PREFIX: ${ERMITE_S3_PREFIX:-} | |
| 27 | 35 | volumes: | |
| 28 | 36 | - .:/src | |
| 29 | 37 | - go-mod:/go/pkg/mod | |
| 30 | 38 | - go-build:/root/.cache/go-build | |
| 31 | 39 | - data:/data | |
| 32 | 40 | ||
| 41 | + | minio: | |
| 42 | + | image: minio/minio:RELEASE.2025-09-07T16-13-09Z | |
| 43 | + | profiles: [s3] | |
| 44 | + | command: server /data --console-address :9001 | |
| 45 | + | environment: | |
| 46 | + | MINIO_ROOT_USER: minioadmin | |
| 47 | + | MINIO_ROOT_PASSWORD: minioadmin | |
| 48 | + | ports: | |
| 49 | + | - "127.0.0.1:9000:9000" | |
| 50 | + | - "127.0.0.1:9001:9001" | |
| 51 | + | volumes: | |
| 52 | + | - minio-data:/data | |
| 53 | + | healthcheck: | |
| 54 | + | test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] | |
| 55 | + | interval: 2s | |
| 56 | + | timeout: 2s | |
| 57 | + | retries: 30 | |
| 58 | + | ||
| 59 | + | # Development provisioning only. The ermite process never creates buckets. | |
| 60 | + | minio-init: | |
| 61 | + | image: minio/mc:RELEASE.2025-08-13T08-35-41Z | |
| 62 | + | profiles: [s3] | |
| 63 | + | depends_on: | |
| 64 | + | minio: | |
| 65 | + | condition: service_healthy | |
| 66 | + | entrypoint: ["/bin/sh", "-c"] | |
| 67 | + | command: | |
| 68 | + | - "mc alias set dev http://minio:9000 minioadmin minioadmin && mc mb --ignore-existing dev/ermite && mc mb --ignore-existing dev/ermite-test" | |
| 69 | + | restart: "no" | |
| 70 | + | ||
| 33 | 71 | volumes: | |
| 72 | + | minio-data: | |
| 34 | 73 | go-mod: | |
| 35 | 74 | go-build: | |
| 36 | 75 | data: | |
| @@ -24,13 +24,52 @@ services: | |||||
| 24 | ERMITE_ADMIN_TOKEN: dev-only-admin-token-do-not-use-0123456789 | 24 | ERMITE_ADMIN_TOKEN: dev-only-admin-token-do-not-use-0123456789 | ||
| 25 | ERMITE_LOG_FORMAT: console | 25 | ERMITE_LOG_FORMAT: console | ||
| 26 | ERMITE_LOG_LEVEL: debug | 26 | ERMITE_LOG_LEVEL: debug | ||
| 27 | + | # Empty bucket keeps the disk backend, even with the s3 profile active. | |||
| 28 | + | ERMITE_S3_BUCKET: ${ERMITE_S3_BUCKET:-} | |||
| 29 | + | ERMITE_S3_ENDPOINT: ${ERMITE_S3_ENDPOINT:-} | |||
| 30 | + | ERMITE_S3_REGION: ${ERMITE_S3_REGION:-} | |||
| 31 | + | ERMITE_S3_ACCESS_KEY_ID: ${ERMITE_S3_ACCESS_KEY_ID:-} | |||
| 32 | + | ERMITE_S3_SECRET_ACCESS_KEY: ${ERMITE_S3_SECRET_ACCESS_KEY:-} | |||
| 33 | + | ERMITE_S3_FORCE_PATH_STYLE: ${ERMITE_S3_FORCE_PATH_STYLE:-0} | |||
| 34 | + | ERMITE_S3_PREFIX: ${ERMITE_S3_PREFIX:-} | |||
| 27 | volumes: | 35 | volumes: | ||
| 28 | - .:/src | 36 | - .:/src | ||
| 29 | - go-mod:/go/pkg/mod | 37 | - go-mod:/go/pkg/mod | ||
| 30 | - go-build:/root/.cache/go-build | 38 | - go-build:/root/.cache/go-build | ||
| 31 | - data:/data | 39 | - data:/data | ||
| 32 | 40 | ||||
| 41 | + | minio: | |||
| 42 | + | image: minio/minio:RELEASE.2025-09-07T16-13-09Z | |||
| 43 | + | profiles: [s3] | |||
| 44 | + | command: server /data --console-address :9001 | |||
| 45 | + | environment: | |||
| 46 | + | MINIO_ROOT_USER: minioadmin | |||
| 47 | + | MINIO_ROOT_PASSWORD: minioadmin | |||
| 48 | + | ports: | |||
| 49 | + | - "127.0.0.1:9000:9000" | |||
| 50 | + | - "127.0.0.1:9001:9001" | |||
| 51 | + | volumes: | |||
| 52 | + | - minio-data:/data | |||
| 53 | + | healthcheck: | |||
| 54 | + | test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] | |||
| 55 | + | interval: 2s | |||
| 56 | + | timeout: 2s | |||
| 57 | + | retries: 30 | |||
| 58 | + | ||||
| 59 | + | # Development provisioning only. The ermite process never creates buckets. | |||
| 60 | + | minio-init: | |||
| 61 | + | image: minio/mc:RELEASE.2025-08-13T08-35-41Z | |||
| 62 | + | profiles: [s3] | |||
| 63 | + | depends_on: | |||
| 64 | + | minio: | |||
| 65 | + | condition: service_healthy | |||
| 66 | + | entrypoint: ["/bin/sh", "-c"] | |||
| 67 | + | command: | |||
| 68 | + | - "mc alias set dev http://minio:9000 minioadmin minioadmin && mc mb --ignore-existing dev/ermite && mc mb --ignore-existing dev/ermite-test" | |||
| 69 | + | restart: "no" | |||
| 70 | + | ||||
| 33 | volumes: | 71 | volumes: | ||
| 72 | + | minio-data: | |||
| 34 | go-mod: | 73 | go-mod: | ||
| 35 | go-build: | 74 | go-build: | ||
| 36 | data: | 75 | data: | ||
M docs/ci-artifacts.md +2 -1
| @@ -13,7 +13,8 @@ The run page lists them with download links. | |||
| 13 | 13 | ||
| 14 | 14 | - A job sees only the artifacts of its own run. A re-run that uploads again replaces the earlier one. | |
| 15 | 15 | - Never served anonymously, even for a public repository: `repo:read` to list and download, `ci:write` to delete. | |
| 16 | - | - Stored under `$ERMITE_DATA_DIR/ci/artifacts`. Removed when they expire, or with their run or repository. | |
| 16 | + | - Contents use the shared [blob store](storage.md): `$ERMITE_DATA_DIR/blobs/objects/ci/` by default, or the configured S3 bucket/prefix. Metadata stays in kv. Removed when they expire, or with their run or repository. | |
| 17 | + | - Legacy artifacts under `$ERMITE_DATA_DIR/ci/artifacts` remain readable until expiry, with no copy or migration. Keep that tree until its last artifact expires. Changing backends does not move blob-backed artifacts; keep the backend stable while they are live. | |
| 17 | 18 | - An upload not finished when its job ends is dropped. | |
| 18 | 19 | ||
| 19 | 20 | ## Limits | |
| @@ -13,7 +13,8 @@ The run page lists them with download links. | |||||
| 13 | 13 | ||||
| 14 | - A job sees only the artifacts of its own run. A re-run that uploads again replaces the earlier one. | 14 | - A job sees only the artifacts of its own run. A re-run that uploads again replaces the earlier one. | ||
| 15 | - Never served anonymously, even for a public repository: `repo:read` to list and download, `ci:write` to delete. | 15 | - Never served anonymously, even for a public repository: `repo:read` to list and download, `ci:write` to delete. | ||
| 16 | - | - Stored under `$ERMITE_DATA_DIR/ci/artifacts`. Removed when they expire, or with their run or repository. | 16 | + | - Contents use the shared [blob store](storage.md): `$ERMITE_DATA_DIR/blobs/objects/ci/` by default, or the configured S3 bucket/prefix. Metadata stays in kv. Removed when they expire, or with their run or repository. |
| 17 | + | - Legacy artifacts under `$ERMITE_DATA_DIR/ci/artifacts` remain readable until expiry, with no copy or migration. Keep that tree until its last artifact expires. Changing backends does not move blob-backed artifacts; keep the backend stable while they are live. | |||
| 17 | - An upload not finished when its job ends is dropped. | 18 | - An upload not finished when its job ends is dropped. | ||
| 18 | 19 | ||||
| 19 | ## Limits | 20 | ## Limits | ||
M docs/configuration.md +29 -1
| @@ -60,10 +60,38 @@ head -c 32 /dev/urandom | base64 | |||
| 60 | 60 | - The server refuses to start with a different key than the one the secrets were sealed with. | |
| 61 | 61 | - Losing the key loses every secret. A backup without the key reveals none. | |
| 62 | 62 | ||
| 63 | + | ## Blob storage and releases | |
| 64 | + | ||
| 65 | + | | Variable | Default | Notes | | |
| 66 | + | |---|---|---| | |
| 67 | + | | `ERMITE_S3_BUCKET` | empty | selects S3 when nonempty; otherwise disk under `$ERMITE_DATA_DIR/blobs` | | |
| 68 | + | | `ERMITE_S3_ENDPOINT` | empty | http(s) endpoint; empty uses AWS region resolution; no credentials, path, query or fragment | | |
| 69 | + | | `ERMITE_S3_REGION` | empty | required with a bucket, including custom endpoints | | |
| 70 | + | | `ERMITE_S3_ACCESS_KEY_ID` | empty | explicit key, required with a bucket | | |
| 71 | + | | `ERMITE_S3_SECRET_ACCESS_KEY` | empty | explicit secret, required with a bucket | | |
| 72 | + | | `ERMITE_S3_FORCE_PATH_STYLE` | `0` | `0`/`1` or `false`/`true`; `1` for MinIO without wildcard DNS | | |
| 73 | + | | `ERMITE_S3_PREFIX` | empty | ASCII key prefix, normalized to a trailing `/`; no empty, `.` or `..` segments | | |
| 74 | + | | `ERMITE_RELEASE_ASSET_MAX` | `2GiB` | one release asset | | |
| 75 | + | | `ERMITE_RELEASE_ASSETS_MAX` | `1000` | positive integer, per release | | |
| 76 | + | | `ERMITE_RELEASE_QUOTA` | `20GiB` | per repository, including uploads in progress | | |
| 77 | + | ||
| 78 | + | Sizes accept bytes or `KiB`, `MiB`, `GiB`, `TiB`, from 1 MiB to 1 PiB. | |
| 79 | + | See [storage](storage.md) for AWS, Scaleway, R2, MinIO, permissions and backups, | |
| 80 | + | and [releases](releases.md) for publishing. | |
| 81 | + | ||
| 82 | + | ### Secret files | |
| 83 | + | ||
| 84 | + | `ERMITE_ADMIN_TOKEN`, `ERMITE_SECRETS_KEY`, `ERMITE_S3_ACCESS_KEY_ID` and | |
| 85 | + | `ERMITE_S3_SECRET_ACCESS_KEY` each support a `_FILE` variant, for example | |
| 86 | + | `ERMITE_ADMIN_TOKEN_FILE=/run/secrets/admin-token`. Set either the direct value | |
| 87 | + | or its file, never both. Files are read once at startup; trailing CR/LF is | |
| 88 | + | removed. Empty or unreadable files fail validation. Secret contents and file | |
| 89 | + | paths are not included in file-resolution errors. No AWS credential chain is used. | |
| 90 | + | ||
| 63 | 91 | ## Runtime files | |
| 64 | 92 | ||
| 65 | 93 | The hook socket and scripts git calls on each push live in `$ERMITE_DATA_DIR/run/`. The socket path must stay under about 100 bytes, so keep `ERMITE_DATA_DIR` short. | |
| 66 | 94 | ||
| 67 | 95 | ## Storage schema | |
| 68 | 96 | ||
| 69 | - | This build uses storage schema 4. Once it has run on your data, an older ermite refuses to start. To downgrade, restore the backup taken before the upgrade. | |
| 97 | + | This build uses storage schema 5. Once it has run on your data, an older ermite refuses to start. To downgrade, restore the backup taken before the upgrade. | |
| @@ -60,10 +60,38 @@ head -c 32 /dev/urandom | base64 | |||||
| 60 | - The server refuses to start with a different key than the one the secrets were sealed with. | 60 | - The server refuses to start with a different key than the one the secrets were sealed with. | ||
| 61 | - Losing the key loses every secret. A backup without the key reveals none. | 61 | - Losing the key loses every secret. A backup without the key reveals none. | ||
| 62 | 62 | ||||
| 63 | + | ## Blob storage and releases | |||
| 64 | + | ||||
| 65 | + | | Variable | Default | Notes | | |||
| 66 | + | |---|---|---| | |||
| 67 | + | | `ERMITE_S3_BUCKET` | empty | selects S3 when nonempty; otherwise disk under `$ERMITE_DATA_DIR/blobs` | | |||
| 68 | + | | `ERMITE_S3_ENDPOINT` | empty | http(s) endpoint; empty uses AWS region resolution; no credentials, path, query or fragment | | |||
| 69 | + | | `ERMITE_S3_REGION` | empty | required with a bucket, including custom endpoints | | |||
| 70 | + | | `ERMITE_S3_ACCESS_KEY_ID` | empty | explicit key, required with a bucket | | |||
| 71 | + | | `ERMITE_S3_SECRET_ACCESS_KEY` | empty | explicit secret, required with a bucket | | |||
| 72 | + | | `ERMITE_S3_FORCE_PATH_STYLE` | `0` | `0`/`1` or `false`/`true`; `1` for MinIO without wildcard DNS | | |||
| 73 | + | | `ERMITE_S3_PREFIX` | empty | ASCII key prefix, normalized to a trailing `/`; no empty, `.` or `..` segments | | |||
| 74 | + | | `ERMITE_RELEASE_ASSET_MAX` | `2GiB` | one release asset | | |||
| 75 | + | | `ERMITE_RELEASE_ASSETS_MAX` | `1000` | positive integer, per release | | |||
| 76 | + | | `ERMITE_RELEASE_QUOTA` | `20GiB` | per repository, including uploads in progress | | |||
| 77 | + | ||||
| 78 | + | Sizes accept bytes or `KiB`, `MiB`, `GiB`, `TiB`, from 1 MiB to 1 PiB. | |||
| 79 | + | See [storage](storage.md) for AWS, Scaleway, R2, MinIO, permissions and backups, | |||
| 80 | + | and [releases](releases.md) for publishing. | |||
| 81 | + | ||||
| 82 | + | ### Secret files | |||
| 83 | + | ||||
| 84 | + | `ERMITE_ADMIN_TOKEN`, `ERMITE_SECRETS_KEY`, `ERMITE_S3_ACCESS_KEY_ID` and | |||
| 85 | + | `ERMITE_S3_SECRET_ACCESS_KEY` each support a `_FILE` variant, for example | |||
| 86 | + | `ERMITE_ADMIN_TOKEN_FILE=/run/secrets/admin-token`. Set either the direct value | |||
| 87 | + | or its file, never both. Files are read once at startup; trailing CR/LF is | |||
| 88 | + | removed. Empty or unreadable files fail validation. Secret contents and file | |||
| 89 | + | paths are not included in file-resolution errors. No AWS credential chain is used. | |||
| 90 | + | ||||
| 63 | ## Runtime files | 91 | ## Runtime files | ||
| 64 | 92 | ||||
| 65 | The hook socket and scripts git calls on each push live in `$ERMITE_DATA_DIR/run/`. The socket path must stay under about 100 bytes, so keep `ERMITE_DATA_DIR` short. | 93 | The hook socket and scripts git calls on each push live in `$ERMITE_DATA_DIR/run/`. The socket path must stay under about 100 bytes, so keep `ERMITE_DATA_DIR` short. | ||
| 66 | 94 | ||||
| 67 | ## Storage schema | 95 | ## Storage schema | ||
| 68 | 96 | ||||
| 69 | - | This build uses storage schema 4. Once it has run on your data, an older ermite refuses to start. To downgrade, restore the backup taken before the upgrade. | 97 | + | This build uses storage schema 5. Once it has run on your data, an older ermite refuses to start. To downgrade, restore the backup taken before the upgrade. |
A docs/releases.md +99 -0
| @@ -0,0 +1,99 @@ | |||
| 1 | + | # Releases | |
| 2 | + | ||
| 3 | + | A release binds a Git tag to a title, Markdown notes and named assets. There is | |
| 4 | + | one release per tag. Published releases follow repository visibility; drafts | |
| 5 | + | are visible only to writers. The web Releases tab is read-only. | |
| 6 | + | ||
| 7 | + | ```sh | |
| 8 | + | ermite release create app v1.0.0 --target main --title 'Version 1' --notes-file notes.md --asset dist/app | |
| 9 | + | ermite release list app --json | |
| 10 | + | ermite release view app latest | |
| 11 | + | ermite release edit app v1.0.0 --notes 'Updated notes' | |
| 12 | + | ermite release upload app v1.0.0 dist/app --clobber | |
| 13 | + | ermite release download app latest --pattern 'app*' --dir downloads | |
| 14 | + | ermite release delete app v1.0.0 --yes | |
| 15 | + | ``` | |
| 16 | + | ||
| 17 | + | Create/edit accept `--draft`, `--prerelease`, `--latest`, `--target` and notes. | |
| 18 | + | Use `edit --publish` to publish a draft, `--no-prerelease` to clear that flag. | |
| 19 | + | An existing tag never moves; `--target` creates a missing tag. A draft can name a | |
| 20 | + | missing tag. Deleting a tag turns its release into a draft and keeps its assets; | |
| 21 | + | recreating the tag does not republish it. Deleting a release does not delete the tag. | |
| 22 | + | ||
| 23 | + | `latest` is the newest published non-prerelease by creation time and id, unless | |
| 24 | + | pinned with `--latest` / REST `make_latest: "true"`. Drafting or deleting the | |
| 25 | + | pinned release clears the pin. Asset names are unique within a release, 1–255 | |
| 26 | + | characters, without slashes, backslashes or control characters. CLI `--clobber` | |
| 27 | + | replaces a same-named asset; REST publishers delete it then upload again. | |
| 28 | + | ||
| 29 | + | Writes require `release:write` (included in the `cli` preset), or a CI job token | |
| 30 | + | with `permissions: contents: write` for that repository. Branch-limited tokens | |
| 31 | + | cannot write releases. Read access to a private repository needs `repo:read`; | |
| 32 | + | unreadable releases, including drafts, answer not found. Older servers without | |
| 33 | + | the `releases` feature are refused by the CLI. | |
| 34 | + | ||
| 35 | + | ## Limits and storage | |
| 36 | + | ||
| 37 | + | | Variable | Default | | |
| 38 | + | |---|---| | |
| 39 | + | | `ERMITE_RELEASE_ASSET_MAX` | `2GiB` per asset | | |
| 40 | + | | `ERMITE_RELEASE_ASSETS_MAX` | `1000` per release | | |
| 41 | + | | `ERMITE_RELEASE_QUOTA` | `20GiB` per repository | | |
| 42 | + | ||
| 43 | + | Quota includes uploads in progress. Exceeding limits gives REST 422 or RPC | |
| 44 | + | `resource_exhausted`. Assets use the shared [blob store](storage.md), disk by | |
| 45 | + | default or S3. Downloads pass through ermite, never redirect to the bucket, and | |
| 46 | + | support single byte ranges with 206, Content-Length and Content-Disposition. | |
| 47 | + | Stable links: | |
| 48 | + | ||
| 49 | + | - `/<repo>/releases/download/<tag>/<asset>` | |
| 50 | + | - `/<repo>/releases/latest/download/<asset>` | |
| 51 | + | ||
| 52 | + | ## GitHub-compatible publishers | |
| 53 | + | ||
| 54 | + | REST is at `<public URL>/-/github/api`, uploads at | |
| 55 | + | `<public URL>/-/github/api/uploads`. Use the external `ERMITE_PUBLIC_URL`, not | |
| 56 | + | the S3 endpoint. Stored-token publishers need `release:write` and, for private | |
| 57 | + | repository reads, `repo:read`. The owner component is `ERMITE_OWNER` (`owner` by | |
| 58 | + | default). | |
| 59 | + | ||
| 60 | + | GoReleaser configuration (replace the host and repository): | |
| 61 | + | ||
| 62 | + | ```yaml | |
| 63 | + | version: 2 | |
| 64 | + | github_urls: | |
| 65 | + | api: https://git.example.org/-/github/api/ | |
| 66 | + | upload: https://git.example.org/-/github/api/uploads/ | |
| 67 | + | download: https://git.example.org/ | |
| 68 | + | release: | |
| 69 | + | github: | |
| 70 | + | owner: owner | |
| 71 | + | name: app | |
| 72 | + | ``` | |
| 73 | + | ||
| 74 | + | Run `GITHUB_TOKEN=<token> goreleaser release --clean` from a tagged checkout. | |
| 75 | + | ||
| 76 | + | On an ermite runner, the job supplies `GITHUB_API_URL`, `GITHUB_SERVER_URL` and | |
| 77 | + | `GITHUB_TOKEN`; no endpoint override is needed for action-gh-release: | |
| 78 | + | ||
| 79 | + | ```yaml | |
| 80 | + | name: release | |
| 81 | + | on: | |
| 82 | + | push: | |
| 83 | + | tags: ['v*'] | |
| 84 | + | permissions: | |
| 85 | + | contents: write | |
| 86 | + | jobs: | |
| 87 | + | publish: | |
| 88 | + | runs-on: ubuntu-latest | |
| 89 | + | steps: | |
| 90 | + | - uses: actions/checkout@v4 | |
| 91 | + | - run: mkdir -p dist && printf 'hello\n' > dist/app.txt | |
| 92 | + | - uses: softprops/action-gh-release@v2 | |
| 93 | + | with: | |
| 94 | + | files: dist/* | |
| 95 | + | body: Release built by CI | |
| 96 | + | ``` | |
| 97 | + | ||
| 98 | + | `contents: read` is insufficient and gets 403. Automatic release-note generation, | |
| 99 | + | discussions, reactions, immutability and attestations are not supported. | |
| @@ -0,0 +1,99 @@ | |||||
| 1 | + | # Releases | |||
| 2 | + | ||||
| 3 | + | A release binds a Git tag to a title, Markdown notes and named assets. There is | |||
| 4 | + | one release per tag. Published releases follow repository visibility; drafts | |||
| 5 | + | are visible only to writers. The web Releases tab is read-only. | |||
| 6 | + | ||||
| 7 | + | ```sh | |||
| 8 | + | ermite release create app v1.0.0 --target main --title 'Version 1' --notes-file notes.md --asset dist/app | |||
| 9 | + | ermite release list app --json | |||
| 10 | + | ermite release view app latest | |||
| 11 | + | ermite release edit app v1.0.0 --notes 'Updated notes' | |||
| 12 | + | ermite release upload app v1.0.0 dist/app --clobber | |||
| 13 | + | ermite release download app latest --pattern 'app*' --dir downloads | |||
| 14 | + | ermite release delete app v1.0.0 --yes | |||
| 15 | + | ``` | |||
| 16 | + | ||||
| 17 | + | Create/edit accept `--draft`, `--prerelease`, `--latest`, `--target` and notes. | |||
| 18 | + | Use `edit --publish` to publish a draft, `--no-prerelease` to clear that flag. | |||
| 19 | + | An existing tag never moves; `--target` creates a missing tag. A draft can name a | |||
| 20 | + | missing tag. Deleting a tag turns its release into a draft and keeps its assets; | |||
| 21 | + | recreating the tag does not republish it. Deleting a release does not delete the tag. | |||
| 22 | + | ||||
| 23 | + | `latest` is the newest published non-prerelease by creation time and id, unless | |||
| 24 | + | pinned with `--latest` / REST `make_latest: "true"`. Drafting or deleting the | |||
| 25 | + | pinned release clears the pin. Asset names are unique within a release, 1–255 | |||
| 26 | + | characters, without slashes, backslashes or control characters. CLI `--clobber` | |||
| 27 | + | replaces a same-named asset; REST publishers delete it then upload again. | |||
| 28 | + | ||||
| 29 | + | Writes require `release:write` (included in the `cli` preset), or a CI job token | |||
| 30 | + | with `permissions: contents: write` for that repository. Branch-limited tokens | |||
| 31 | + | cannot write releases. Read access to a private repository needs `repo:read`; | |||
| 32 | + | unreadable releases, including drafts, answer not found. Older servers without | |||
| 33 | + | the `releases` feature are refused by the CLI. | |||
| 34 | + | ||||
| 35 | + | ## Limits and storage | |||
| 36 | + | ||||
| 37 | + | | Variable | Default | | |||
| 38 | + | |---|---| | |||
| 39 | + | | `ERMITE_RELEASE_ASSET_MAX` | `2GiB` per asset | | |||
| 40 | + | | `ERMITE_RELEASE_ASSETS_MAX` | `1000` per release | | |||
| 41 | + | | `ERMITE_RELEASE_QUOTA` | `20GiB` per repository | | |||
| 42 | + | ||||
| 43 | + | Quota includes uploads in progress. Exceeding limits gives REST 422 or RPC | |||
| 44 | + | `resource_exhausted`. Assets use the shared [blob store](storage.md), disk by | |||
| 45 | + | default or S3. Downloads pass through ermite, never redirect to the bucket, and | |||
| 46 | + | support single byte ranges with 206, Content-Length and Content-Disposition. | |||
| 47 | + | Stable links: | |||
| 48 | + | ||||
| 49 | + | - `/<repo>/releases/download/<tag>/<asset>` | |||
| 50 | + | - `/<repo>/releases/latest/download/<asset>` | |||
| 51 | + | ||||
| 52 | + | ## GitHub-compatible publishers | |||
| 53 | + | ||||
| 54 | + | REST is at `<public URL>/-/github/api`, uploads at | |||
| 55 | + | `<public URL>/-/github/api/uploads`. Use the external `ERMITE_PUBLIC_URL`, not | |||
| 56 | + | the S3 endpoint. Stored-token publishers need `release:write` and, for private | |||
| 57 | + | repository reads, `repo:read`. The owner component is `ERMITE_OWNER` (`owner` by | |||
| 58 | + | default). | |||
| 59 | + | ||||
| 60 | + | GoReleaser configuration (replace the host and repository): | |||
| 61 | + | ||||
| 62 | + | ```yaml | |||
| 63 | + | version: 2 | |||
| 64 | + | github_urls: | |||
| 65 | + | api: https://git.example.org/-/github/api/ | |||
| 66 | + | upload: https://git.example.org/-/github/api/uploads/ | |||
| 67 | + | download: https://git.example.org/ | |||
| 68 | + | release: | |||
| 69 | + | github: | |||
| 70 | + | owner: owner | |||
| 71 | + | name: app | |||
| 72 | + | ``` | |||
| 73 | + | ||||
| 74 | + | Run `GITHUB_TOKEN=<token> goreleaser release --clean` from a tagged checkout. | |||
| 75 | + | ||||
| 76 | + | On an ermite runner, the job supplies `GITHUB_API_URL`, `GITHUB_SERVER_URL` and | |||
| 77 | + | `GITHUB_TOKEN`; no endpoint override is needed for action-gh-release: | |||
| 78 | + | ||||
| 79 | + | ```yaml | |||
| 80 | + | name: release | |||
| 81 | + | on: | |||
| 82 | + | push: | |||
| 83 | + | tags: ['v*'] | |||
| 84 | + | permissions: | |||
| 85 | + | contents: write | |||
| 86 | + | jobs: | |||
| 87 | + | publish: | |||
| 88 | + | runs-on: ubuntu-latest | |||
| 89 | + | steps: | |||
| 90 | + | - uses: actions/checkout@v4 | |||
| 91 | + | - run: mkdir -p dist && printf 'hello\n' > dist/app.txt | |||
| 92 | + | - uses: softprops/action-gh-release@v2 | |||
| 93 | + | with: | |||
| 94 | + | files: dist/* | |||
| 95 | + | body: Release built by CI | |||
| 96 | + | ``` | |||
| 97 | + | ||||
| 98 | + | `contents: read` is insufficient and gets 403. Automatic release-note generation, | |||
| 99 | + | discussions, reactions, immutability and attestations are not supported. | |||
A docs/storage.md +89 -0
| @@ -0,0 +1,89 @@ | |||
| 1 | + | # Blob storage | |
| 2 | + | ||
| 3 | + | Release assets and CI artifact contents share one blob store. Metadata stays in | |
| 4 | + | `$ERMITE_DATA_DIR/kv`; Git repositories and CI logs stay on local disk. | |
| 5 | + | ||
| 6 | + | With no `ERMITE_S3_BUCKET`, objects live under | |
| 7 | + | `$ERMITE_DATA_DIR/blobs/objects/`. Directories are private (0700), files 0600. | |
| 8 | + | Back up the entire data directory while the server is stopped. With S3, back up | |
| 9 | + | both the data directory and the bucket prefix consistently. | |
| 10 | + | ||
| 11 | + | ## S3-compatible storage | |
| 12 | + | ||
| 13 | + | Create a bucket yourself, then configure ermite. All examples use explicit keys: | |
| 14 | + | ||
| 15 | + | ```sh | |
| 16 | + | export ERMITE_S3_BUCKET=forge-assets | |
| 17 | + | export ERMITE_S3_PREFIX=ermite/blobs | |
| 18 | + | export ERMITE_S3_ACCESS_KEY_ID_FILE=/run/secrets/s3-id | |
| 19 | + | export ERMITE_S3_SECRET_ACCESS_KEY_FILE=/run/secrets/s3-secret | |
| 20 | + | ``` | |
| 21 | + | ||
| 22 | + | Select the endpoint and signing region for your provider: | |
| 23 | + | ||
| 24 | + | | Service | `ERMITE_S3_ENDPOINT` | `ERMITE_S3_REGION` | `ERMITE_S3_FORCE_PATH_STYLE` | | |
| 25 | + | |---|---|---|---| | |
| 26 | + | | AWS S3 | unset (SDK resolves from region) | bucket region, e.g. `eu-west-1` | `0` | | |
| 27 | + | | Scaleway | `https://s3.fr-par.scw.cloud` | `fr-par` | `0` | | |
| 28 | + | | Cloudflare R2 | `https://<account-id>.r2.cloudflarestorage.com` | `auto` | `0` | | |
| 29 | + | | MinIO (compose network) | `http://minio:9000` | `us-east-1` | `1` | | |
| 30 | + | ||
| 31 | + | Any S3-compatible endpoint implementing the subset below can be used. There is | |
| 32 | + | no ambient AWS credential chain: `AWS_*`, shared credential files and instance | |
| 33 | + | roles are not consulted. Region and both keys are required with a bucket. | |
| 34 | + | Each key supports `_FILE`; direct value and file are mutually exclusive. | |
| 35 | + | ||
| 36 | + | On startup, ermite probes the store (disk write probe; S3 `HeadBucket`, falling | |
| 37 | + | back to `ListObjectsV2` with one key on 403). Failure stops startup and names | |
| 38 | + | `ERMITE_S3_BUCKET`. Success logs `blob store ready`, never credential values. | |
| 39 | + | The S3 probe checks reachability/read permission, not every write permission. | |
| 40 | + | ||
| 41 | + | Grant access to objects under the configured prefix: put, get/head, delete, | |
| 42 | + | list, and create/upload/complete/abort/list multipart uploads. On AWS these are | |
| 43 | + | `s3:PutObject`, `s3:GetObject`, `s3:DeleteObject`, | |
| 44 | + | `s3:AbortMultipartUpload`, `s3:ListMultipartUploadParts` on the object resource, | |
| 45 | + | and `s3:ListBucket`, `s3:ListBucketMultipartUploads` on the bucket resource. | |
| 46 | + | Constrain listing to the prefix where supported. ermite never creates buckets or | |
| 47 | + | calls bucket administration APIs (policy, ACL, tagging, lifecycle, versioning). | |
| 48 | + | The client uses checksums only when required, not automatic CRC32 on every PUT. | |
| 49 | + | ||
| 50 | + | Downloads are relayed through ermite with repository authorization and Range | |
| 51 | + | support: no redirects or presigned URLs. Multipart uploads use 8 MiB parts. | |
| 52 | + | The daily sweep removes unreferenced blobs and unfinished uploads older than | |
| 53 | + | 24 hours. Release metadata with a missing blob is removed only after two sweeps. | |
| 54 | + | ||
| 55 | + | ## Backend changes | |
| 56 | + | ||
| 57 | + | Legacy artifacts under `$ERMITE_DATA_DIR/ci/artifacts` remain readable until | |
| 58 | + | expiry; there is no automatic copy. New artifacts and all release assets use the | |
| 59 | + | currently selected backend. **Do not switch backends with live blob-backed data**: | |
| 60 | + | records identify blobs, not their backend. Switching disk to S3, changing bucket | |
| 61 | + | or prefix, or switching back makes earlier blob-backed contents unavailable. | |
| 62 | + | Keep the selected backend stable, or arrange a complete offline copy yourself. | |
| 63 | + | ||
| 64 | + | ## Development compose | |
| 65 | + | ||
| 66 | + | Disk (no MinIO services): | |
| 67 | + | ||
| 68 | + | ```sh | |
| 69 | + | ERMITE_S3_BUCKET= docker compose up | |
| 70 | + | ``` | |
| 71 | + | ||
| 72 | + | MinIO provisioning must finish before the forge starts its probe: | |
| 73 | + | ||
| 74 | + | ```sh | |
| 75 | + | docker compose --profile s3 up -d minio | |
| 76 | + | docker compose --profile s3 run --rm minio-init | |
| 77 | + | ERMITE_S3_BUCKET=ermite ERMITE_S3_ENDPOINT=http://minio:9000 \ | |
| 78 | + | ERMITE_S3_REGION=us-east-1 ERMITE_S3_ACCESS_KEY_ID=minioadmin \ | |
| 79 | + | ERMITE_S3_SECRET_ACCESS_KEY=minioadmin ERMITE_S3_FORCE_PATH_STYLE=1 \ | |
| 80 | + | ERMITE_S3_PREFIX=dev/ docker compose --profile s3 up ermite | |
| 81 | + | ``` | |
| 82 | + | ||
| 83 | + | Console: <http://localhost:9001>, development credentials `minioadmin` / | |
| 84 | + | `minioadmin`. The init service creates `ermite` and `ermite-test`; only that | |
| 85 | + | provisioning service has bucket creation duties. For Scaleway or another external | |
| 86 | + | service, use the same environment variables with its existing bucket, endpoint, | |
| 87 | + | region and keys, and `docker compose up ermite` without the profile. These are | |
| 88 | + | development settings; never use the placeholder admin token or MinIO keys in | |
| 89 | + | production. | |
| @@ -0,0 +1,89 @@ | |||||
| 1 | + | # Blob storage | |||
| 2 | + | ||||
| 3 | + | Release assets and CI artifact contents share one blob store. Metadata stays in | |||
| 4 | + | `$ERMITE_DATA_DIR/kv`; Git repositories and CI logs stay on local disk. | |||
| 5 | + | ||||
| 6 | + | With no `ERMITE_S3_BUCKET`, objects live under | |||
| 7 | + | `$ERMITE_DATA_DIR/blobs/objects/`. Directories are private (0700), files 0600. | |||
| 8 | + | Back up the entire data directory while the server is stopped. With S3, back up | |||
| 9 | + | both the data directory and the bucket prefix consistently. | |||
| 10 | + | ||||
| 11 | + | ## S3-compatible storage | |||
| 12 | + | ||||
| 13 | + | Create a bucket yourself, then configure ermite. All examples use explicit keys: | |||
| 14 | + | ||||
| 15 | + | ```sh | |||
| 16 | + | export ERMITE_S3_BUCKET=forge-assets | |||
| 17 | + | export ERMITE_S3_PREFIX=ermite/blobs | |||
| 18 | + | export ERMITE_S3_ACCESS_KEY_ID_FILE=/run/secrets/s3-id | |||
| 19 | + | export ERMITE_S3_SECRET_ACCESS_KEY_FILE=/run/secrets/s3-secret | |||
| 20 | + | ``` | |||
| 21 | + | ||||
| 22 | + | Select the endpoint and signing region for your provider: | |||
| 23 | + | ||||
| 24 | + | | Service | `ERMITE_S3_ENDPOINT` | `ERMITE_S3_REGION` | `ERMITE_S3_FORCE_PATH_STYLE` | | |||
| 25 | + | |---|---|---|---| | |||
| 26 | + | | AWS S3 | unset (SDK resolves from region) | bucket region, e.g. `eu-west-1` | `0` | | |||
| 27 | + | | Scaleway | `https://s3.fr-par.scw.cloud` | `fr-par` | `0` | | |||
| 28 | + | | Cloudflare R2 | `https://<account-id>.r2.cloudflarestorage.com` | `auto` | `0` | | |||
| 29 | + | | MinIO (compose network) | `http://minio:9000` | `us-east-1` | `1` | | |||
| 30 | + | ||||
| 31 | + | Any S3-compatible endpoint implementing the subset below can be used. There is | |||
| 32 | + | no ambient AWS credential chain: `AWS_*`, shared credential files and instance | |||
| 33 | + | roles are not consulted. Region and both keys are required with a bucket. | |||
| 34 | + | Each key supports `_FILE`; direct value and file are mutually exclusive. | |||
| 35 | + | ||||
| 36 | + | On startup, ermite probes the store (disk write probe; S3 `HeadBucket`, falling | |||
| 37 | + | back to `ListObjectsV2` with one key on 403). Failure stops startup and names | |||
| 38 | + | `ERMITE_S3_BUCKET`. Success logs `blob store ready`, never credential values. | |||
| 39 | + | The S3 probe checks reachability/read permission, not every write permission. | |||
| 40 | + | ||||
| 41 | + | Grant access to objects under the configured prefix: put, get/head, delete, | |||
| 42 | + | list, and create/upload/complete/abort/list multipart uploads. On AWS these are | |||
| 43 | + | `s3:PutObject`, `s3:GetObject`, `s3:DeleteObject`, | |||
| 44 | + | `s3:AbortMultipartUpload`, `s3:ListMultipartUploadParts` on the object resource, | |||
| 45 | + | and `s3:ListBucket`, `s3:ListBucketMultipartUploads` on the bucket resource. | |||
| 46 | + | Constrain listing to the prefix where supported. ermite never creates buckets or | |||
| 47 | + | calls bucket administration APIs (policy, ACL, tagging, lifecycle, versioning). | |||
| 48 | + | The client uses checksums only when required, not automatic CRC32 on every PUT. | |||
| 49 | + | ||||
| 50 | + | Downloads are relayed through ermite with repository authorization and Range | |||
| 51 | + | support: no redirects or presigned URLs. Multipart uploads use 8 MiB parts. | |||
| 52 | + | The daily sweep removes unreferenced blobs and unfinished uploads older than | |||
| 53 | + | 24 hours. Release metadata with a missing blob is removed only after two sweeps. | |||
| 54 | + | ||||
| 55 | + | ## Backend changes | |||
| 56 | + | ||||
| 57 | + | Legacy artifacts under `$ERMITE_DATA_DIR/ci/artifacts` remain readable until | |||
| 58 | + | expiry; there is no automatic copy. New artifacts and all release assets use the | |||
| 59 | + | currently selected backend. **Do not switch backends with live blob-backed data**: | |||
| 60 | + | records identify blobs, not their backend. Switching disk to S3, changing bucket | |||
| 61 | + | or prefix, or switching back makes earlier blob-backed contents unavailable. | |||
| 62 | + | Keep the selected backend stable, or arrange a complete offline copy yourself. | |||
| 63 | + | ||||
| 64 | + | ## Development compose | |||
| 65 | + | ||||
| 66 | + | Disk (no MinIO services): | |||
| 67 | + | ||||
| 68 | + | ```sh | |||
| 69 | + | ERMITE_S3_BUCKET= docker compose up | |||
| 70 | + | ``` | |||
| 71 | + | ||||
| 72 | + | MinIO provisioning must finish before the forge starts its probe: | |||
| 73 | + | ||||
| 74 | + | ```sh | |||
| 75 | + | docker compose --profile s3 up -d minio | |||
| 76 | + | docker compose --profile s3 run --rm minio-init | |||
| 77 | + | ERMITE_S3_BUCKET=ermite ERMITE_S3_ENDPOINT=http://minio:9000 \ | |||
| 78 | + | ERMITE_S3_REGION=us-east-1 ERMITE_S3_ACCESS_KEY_ID=minioadmin \ | |||
| 79 | + | ERMITE_S3_SECRET_ACCESS_KEY=minioadmin ERMITE_S3_FORCE_PATH_STYLE=1 \ | |||
| 80 | + | ERMITE_S3_PREFIX=dev/ docker compose --profile s3 up ermite | |||
| 81 | + | ``` | |||
| 82 | + | ||||
| 83 | + | Console: <http://localhost:9001>, development credentials `minioadmin` / | |||
| 84 | + | `minioadmin`. The init service creates `ermite` and `ermite-test`; only that | |||
| 85 | + | provisioning service has bucket creation duties. For Scaleway or another external | |||
| 86 | + | service, use the same environment variables with its existing bucket, endpoint, | |||
| 87 | + | region and keys, and `docker compose up ermite` without the profile. These are | |||
| 88 | + | development settings; never use the placeholder admin token or MinIO keys in | |||
| 89 | + | production. | |||
M docs/tokens.md +9 -0
| @@ -15,6 +15,14 @@ The secret is printed once. Presets and `--scope` add up (`issue:write` lets a t | |||
| 15 | 15 | | `ci` | `repo:read` | | |
| 16 | 16 | | `runner` | `runner:register`: a runner host. It can read the code and secrets of every repository, so give it only to runners you operate | | |
| 17 | 17 | ||
| 18 | + | ## Release publishing | |
| 19 | + | ||
| 20 | + | `release:write` permits creating, editing and deleting releases and their assets; | |
| 21 | + | it is included in the `cli` preset, not `agent` or `ci`. Private repository reads | |
| 22 | + | also need `repo:read`. Branch-limited tokens cannot write releases, even with | |
| 23 | + | that scope. CI jobs publish using their job token with `contents: write` under | |
| 24 | + | `permissions`, not a stored-token scope. See [releases](releases.md). | |
| 25 | + | ||
| 18 | 26 | ## Branch-limited tokens | |
| 19 | 27 | ||
| 20 | 28 | For an AI agent that should propose changes, not publish them: | |
| @@ -38,6 +46,7 @@ The token **cannot**: | |||
| 38 | 46 | ||
| 39 | 47 | - push any other ref (`main`, other branches, tags, notes). The whole push is refused and the message names the ref; | |
| 40 | 48 | - create, delete or configure a repository; | |
| 49 | + | - create, edit or delete releases or upload assets; | |
| 41 | 50 | - open web sessions or get owner rights, even with every scope; | |
| 42 | 51 | - manage runners, even with `runner:register`; | |
| 43 | 52 | - get trusted CI runs (no secrets). See [CI trust](ci.md#trust). | |
| @@ -15,6 +15,14 @@ The secret is printed once. Presets and `--scope` add up (`issue:write` lets a t | |||||
| 15 | | `ci` | `repo:read` | | 15 | | `ci` | `repo:read` | | ||
| 16 | | `runner` | `runner:register`: a runner host. It can read the code and secrets of every repository, so give it only to runners you operate | | 16 | | `runner` | `runner:register`: a runner host. It can read the code and secrets of every repository, so give it only to runners you operate | | ||
| 17 | 17 | ||||
| 18 | + | ## Release publishing | |||
| 19 | + | ||||
| 20 | + | `release:write` permits creating, editing and deleting releases and their assets; | |||
| 21 | + | it is included in the `cli` preset, not `agent` or `ci`. Private repository reads | |||
| 22 | + | also need `repo:read`. Branch-limited tokens cannot write releases, even with | |||
| 23 | + | that scope. CI jobs publish using their job token with `contents: write` under | |||
| 24 | + | `permissions`, not a stored-token scope. See [releases](releases.md). | |||
| 25 | + | ||||
| 18 | ## Branch-limited tokens | 26 | ## Branch-limited tokens | ||
| 19 | 27 | ||||
| 20 | For an AI agent that should propose changes, not publish them: | 28 | For an AI agent that should propose changes, not publish them: | ||
| @@ -38,6 +46,7 @@ The token **cannot**: | |||||
| 38 | 46 | ||||
| 39 | - push any other ref (`main`, other branches, tags, notes). The whole push is refused and the message names the ref; | 47 | - push any other ref (`main`, other branches, tags, notes). The whole push is refused and the message names the ref; | ||
| 40 | - create, delete or configure a repository; | 48 | - create, delete or configure a repository; | ||
| 49 | + | - create, edit or delete releases or upload assets; | |||
| 41 | - open web sessions or get owner rights, even with every scope; | 50 | - open web sessions or get owner rights, even with every scope; | ||
| 42 | - manage runners, even with `runner:register`; | 51 | - manage runners, even with `runner:register`; | ||
| 43 | - get trusted CI runs (no secrets). See [CI trust](ci.md#trust). | 52 | - get trusted CI runs (no secrets). See [CI trust](ci.md#trust). | ||
M internal/app/api_test.go +1 -1
| @@ -196,7 +196,7 @@ func TestAC2ReadAccess(t *testing.T) { | |||
| 196 | 196 | for _, who := range allPrincipals { | |
| 197 | 197 | resp, err := r.who[who].server.GetVersion(ctx, connect.NewRequest(&pb.GetVersionRequest{})) | |
| 198 | 198 | require.NoError(t, err, who) | |
| 199 | - | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror", "merge_requests.auto_merge", "merge_requests.ignore_checks", "merge_requests.ready"}, resp.Msg.GetFeatures(), who) | |
| 199 | + | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror", "merge_requests.auto_merge", "merge_requests.ignore_checks", "merge_requests.ready", "releases"}, resp.Msg.GetFeatures(), who) | |
| 200 | 200 | assert.NotEmpty(t, resp.Msg.GetVersion()) | |
| 201 | 201 | } | |
| 202 | 202 | }) | |
| @@ -196,7 +196,7 @@ func TestAC2ReadAccess(t *testing.T) { | |||||
| 196 | for _, who := range allPrincipals { | 196 | for _, who := range allPrincipals { | ||
| 197 | resp, err := r.who[who].server.GetVersion(ctx, connect.NewRequest(&pb.GetVersionRequest{})) | 197 | resp, err := r.who[who].server.GetVersion(ctx, connect.NewRequest(&pb.GetVersionRequest{})) | ||
| 198 | require.NoError(t, err, who) | 198 | require.NoError(t, err, who) | ||
| 199 | - | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror", "merge_requests.auto_merge", "merge_requests.ignore_checks", "merge_requests.ready"}, resp.Msg.GetFeatures(), who) | 199 | + | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror", "merge_requests.auto_merge", "merge_requests.ignore_checks", "merge_requests.ready", "releases"}, resp.Msg.GetFeatures(), who) |
| 200 | assert.NotEmpty(t, resp.Msg.GetVersion()) | 200 | assert.NotEmpty(t, resp.Msg.GetVersion()) | ||
| 201 | } | 201 | } | ||
| 202 | }) | 202 | }) | ||
M internal/app/ci_artifacts_test.go +11 -14
| @@ -49,18 +49,11 @@ jobs: | |||
| 49 | 49 | - run: echo down | |
| 50 | 50 | ` | |
| 51 | 51 | ||
| 52 | - | // newArtifactHarness supplies the P8 blob wiring through the existing test | |
| 53 | - | // hook. Production wiring remains owned by P8. | |
| 52 | + | // newArtifactHarness uses the production blob wiring. | |
| 54 | 53 | func newArtifactHarness(t *testing.T, tweak func(*usecase.CIConfig)) *ciHarness { | |
| 55 | 54 | t.Helper() | |
| 56 | - | root := t.TempDir() | |
| 57 | - | c := newCIHarness(t, func(cfg *usecase.CIConfig) { | |
| 58 | - | cfg.Blobs = blobdisk.New(blobdisk.Dir(root)) | |
| 59 | - | if tweak != nil { | |
| 60 | - | tweak(cfg) | |
| 61 | - | } | |
| 62 | - | }) | |
| 63 | - | c.artifactRoot = blobdisk.Dir(root) | |
| 55 | + | c := newCIHarness(t, tweak) | |
| 56 | + | c.artifactRoot = blobdisk.Dir(c.data) | |
| 64 | 57 | return c | |
| 65 | 58 | } | |
| 66 | 59 | ||
| @@ -583,7 +576,9 @@ func TestCIArtifactLimits(t *testing.T) { | |||
| 583 | 576 | ||
| 584 | 577 | func TestCIArtifactsSweep(t *testing.T) { | |
| 585 | 578 | clk := &shiftClock{} | |
| 579 | + | legacyRoot := ciartifacts.Dir(t.TempDir()) | |
| 586 | 580 | c := newArtifactHarness(t, func(cfg *usecase.CIConfig) { | |
| 581 | + | cfg.ArtifactFiles = ciartifacts.New(legacyRoot) | |
| 587 | 582 | cfg.Clock = clk | |
| 588 | 583 | cfg.Tick = 20 * time.Millisecond | |
| 589 | 584 | cfg.ArtifactLimits.Retention = 2 * 24 * time.Hour | |
| @@ -599,10 +594,10 @@ func TestCIArtifactsSweep(t *testing.T) { | |||
| 599 | 594 | crashed := up.mustCreate("crashed") | |
| 600 | 595 | _, err := up.block(crashed, "x", []byte("stale")) | |
| 601 | 596 | require.NoError(t, err) | |
| 602 | - | stray := filepath.Join(ciartifacts.Dir(c.data), fmt.Sprintf("%020d", runID), fmt.Sprintf("%020d", int64(999999))) | |
| 597 | + | stray := filepath.Join(legacyRoot, fmt.Sprintf("%020d", runID), fmt.Sprintf("%020d", int64(999999))) | |
| 603 | 598 | require.NoError(t, os.MkdirAll(filepath.Dir(stray), 0o700)) | |
| 604 | 599 | require.NoError(t, os.WriteFile(stray, []byte("orphan"), 0o600)) | |
| 605 | - | strayRun := filepath.Join(ciartifacts.Dir(c.data), fmt.Sprintf("%020d", int64(888888))) | |
| 600 | + | strayRun := filepath.Join(legacyRoot, fmt.Sprintf("%020d", int64(888888))) | |
| 606 | 601 | require.NoError(t, os.MkdirAll(strayRun, 0o700)) | |
| 607 | 602 | start := clk.Now() | |
| 608 | 603 | ||
| @@ -807,7 +802,9 @@ func TestCIArtifactCaps(t *testing.T) { | |||
| 807 | 802 | ||
| 808 | 803 | // Both storage generations remain downloadable through the real RPC handlers. | |
| 809 | 804 | func TestCIArtifactBlobAndLegacyDownload(t *testing.T) { | |
| 810 | - | c := newArtifactHarness(t, nil) | |
| 805 | + | // This test inserts legacy data after startup; supply its reader explicitly. | |
| 806 | + | legacyRoot := ciartifacts.Dir(t.TempDir()) | |
| 807 | + | c := newArtifactHarness(t, func(cfg *usecase.CIConfig) { cfg.ArtifactFiles = ciartifacts.New(legacyRoot) }) | |
| 811 | 808 | runner := c.registerRunner("alpha", "ubuntu-latest") | |
| 812 | 809 | _, job, _ := c.newArtifactRun(runner, map[string]string{".github/workflows/ci.yml": artifactWorkflow}) | |
| 813 | 810 | up := c.jobArtifacts(job) | |
| @@ -829,7 +826,7 @@ func TestCIArtifactBlobAndLegacyDownload(t *testing.T) { | |||
| 829 | 826 | legacy := up.upload("old", []byte("old disk")) | |
| 830 | 827 | a, err = store.GetArtifact(c.ctx, legacy.GetId()) | |
| 831 | 828 | require.NoError(t, err) | |
| 832 | - | oldPath := filepath.Join(ciartifacts.Dir(c.data), fmt.Sprintf("%020d", a.RunID), fmt.Sprintf("%020d", a.ID)) | |
| 829 | + | oldPath := filepath.Join(legacyRoot, fmt.Sprintf("%020d", a.RunID), fmt.Sprintf("%020d", a.ID)) | |
| 833 | 830 | require.NoError(t, os.MkdirAll(filepath.Dir(oldPath), 0o700)) | |
| 834 | 831 | require.NoError(t, os.WriteFile(oldPath, []byte("old disk"), 0o600)) | |
| 835 | 832 | require.NoError(t, os.Remove(filepath.Join(c.artifactRoot, "objects", filepath.FromSlash(a.BlobKey())))) | |
| @@ -49,18 +49,11 @@ jobs: | |||||
| 49 | - run: echo down | 49 | - run: echo down | ||
| 50 | ` | 50 | ` | ||
| 51 | 51 | ||||
| 52 | - | // newArtifactHarness supplies the P8 blob wiring through the existing test | 52 | + | // newArtifactHarness uses the production blob wiring. |
| 53 | - | // hook. Production wiring remains owned by P8. | |||
| 54 | func newArtifactHarness(t *testing.T, tweak func(*usecase.CIConfig)) *ciHarness { | 53 | func newArtifactHarness(t *testing.T, tweak func(*usecase.CIConfig)) *ciHarness { | ||
| 55 | t.Helper() | 54 | t.Helper() | ||
| 56 | - | root := t.TempDir() | 55 | + | c := newCIHarness(t, tweak) |
| 57 | - | c := newCIHarness(t, func(cfg *usecase.CIConfig) { | 56 | + | c.artifactRoot = blobdisk.Dir(c.data) |
| 58 | - | cfg.Blobs = blobdisk.New(blobdisk.Dir(root)) | |||
| 59 | - | if tweak != nil { | |||
| 60 | - | tweak(cfg) | |||
| 61 | - | } | |||
| 62 | - | }) | |||
| 63 | - | c.artifactRoot = blobdisk.Dir(root) | |||
| 64 | return c | 57 | return c | ||
| 65 | } | 58 | } | ||
| 66 | 59 | ||||
| @@ -583,7 +576,9 @@ func TestCIArtifactLimits(t *testing.T) { | |||||
| 583 | 576 | ||||
| 584 | func TestCIArtifactsSweep(t *testing.T) { | 577 | func TestCIArtifactsSweep(t *testing.T) { | ||
| 585 | clk := &shiftClock{} | 578 | clk := &shiftClock{} | ||
| 579 | + | legacyRoot := ciartifacts.Dir(t.TempDir()) | |||
| 586 | c := newArtifactHarness(t, func(cfg *usecase.CIConfig) { | 580 | c := newArtifactHarness(t, func(cfg *usecase.CIConfig) { | ||
| 581 | + | cfg.ArtifactFiles = ciartifacts.New(legacyRoot) | |||
| 587 | cfg.Clock = clk | 582 | cfg.Clock = clk | ||
| 588 | cfg.Tick = 20 * time.Millisecond | 583 | cfg.Tick = 20 * time.Millisecond | ||
| 589 | cfg.ArtifactLimits.Retention = 2 * 24 * time.Hour | 584 | cfg.ArtifactLimits.Retention = 2 * 24 * time.Hour | ||
| @@ -599,10 +594,10 @@ func TestCIArtifactsSweep(t *testing.T) { | |||||
| 599 | crashed := up.mustCreate("crashed") | 594 | crashed := up.mustCreate("crashed") | ||
| 600 | _, err := up.block(crashed, "x", []byte("stale")) | 595 | _, err := up.block(crashed, "x", []byte("stale")) | ||
| 601 | require.NoError(t, err) | 596 | require.NoError(t, err) | ||
| 602 | - | stray := filepath.Join(ciartifacts.Dir(c.data), fmt.Sprintf("%020d", runID), fmt.Sprintf("%020d", int64(999999))) | 597 | + | stray := filepath.Join(legacyRoot, fmt.Sprintf("%020d", runID), fmt.Sprintf("%020d", int64(999999))) |
| 603 | require.NoError(t, os.MkdirAll(filepath.Dir(stray), 0o700)) | 598 | require.NoError(t, os.MkdirAll(filepath.Dir(stray), 0o700)) | ||
| 604 | require.NoError(t, os.WriteFile(stray, []byte("orphan"), 0o600)) | 599 | require.NoError(t, os.WriteFile(stray, []byte("orphan"), 0o600)) | ||
| 605 | - | strayRun := filepath.Join(ciartifacts.Dir(c.data), fmt.Sprintf("%020d", int64(888888))) | 600 | + | strayRun := filepath.Join(legacyRoot, fmt.Sprintf("%020d", int64(888888))) |
| 606 | require.NoError(t, os.MkdirAll(strayRun, 0o700)) | 601 | require.NoError(t, os.MkdirAll(strayRun, 0o700)) | ||
| 607 | start := clk.Now() | 602 | start := clk.Now() | ||
| 608 | 603 | ||||
| @@ -807,7 +802,9 @@ func TestCIArtifactCaps(t *testing.T) { | |||||
| 807 | 802 | ||||
| 808 | // Both storage generations remain downloadable through the real RPC handlers. | 803 | // Both storage generations remain downloadable through the real RPC handlers. | ||
| 809 | func TestCIArtifactBlobAndLegacyDownload(t *testing.T) { | 804 | func TestCIArtifactBlobAndLegacyDownload(t *testing.T) { | ||
| 810 | - | c := newArtifactHarness(t, nil) | 805 | + | // This test inserts legacy data after startup; supply its reader explicitly. |
| 806 | + | legacyRoot := ciartifacts.Dir(t.TempDir()) | |||
| 807 | + | c := newArtifactHarness(t, func(cfg *usecase.CIConfig) { cfg.ArtifactFiles = ciartifacts.New(legacyRoot) }) | |||
| 811 | runner := c.registerRunner("alpha", "ubuntu-latest") | 808 | runner := c.registerRunner("alpha", "ubuntu-latest") | ||
| 812 | _, job, _ := c.newArtifactRun(runner, map[string]string{".github/workflows/ci.yml": artifactWorkflow}) | 809 | _, job, _ := c.newArtifactRun(runner, map[string]string{".github/workflows/ci.yml": artifactWorkflow}) | ||
| 813 | up := c.jobArtifacts(job) | 810 | up := c.jobArtifacts(job) | ||
| @@ -829,7 +826,7 @@ func TestCIArtifactBlobAndLegacyDownload(t *testing.T) { | |||||
| 829 | legacy := up.upload("old", []byte("old disk")) | 826 | legacy := up.upload("old", []byte("old disk")) | ||
| 830 | a, err = store.GetArtifact(c.ctx, legacy.GetId()) | 827 | a, err = store.GetArtifact(c.ctx, legacy.GetId()) | ||
| 831 | require.NoError(t, err) | 828 | require.NoError(t, err) | ||
| 832 | - | oldPath := filepath.Join(ciartifacts.Dir(c.data), fmt.Sprintf("%020d", a.RunID), fmt.Sprintf("%020d", a.ID)) | 829 | + | oldPath := filepath.Join(legacyRoot, fmt.Sprintf("%020d", a.RunID), fmt.Sprintf("%020d", a.ID)) |
| 833 | require.NoError(t, os.MkdirAll(filepath.Dir(oldPath), 0o700)) | 830 | require.NoError(t, os.MkdirAll(filepath.Dir(oldPath), 0o700)) | ||
| 834 | require.NoError(t, os.WriteFile(oldPath, []byte("old disk"), 0o600)) | 831 | require.NoError(t, os.WriteFile(oldPath, []byte("old disk"), 0o600)) | ||
| 835 | require.NoError(t, os.Remove(filepath.Join(c.artifactRoot, "objects", filepath.FromSlash(a.BlobKey())))) | 832 | require.NoError(t, os.Remove(filepath.Join(c.artifactRoot, "objects", filepath.FromSlash(a.BlobKey())))) | ||
M internal/app/cli_e2e_test.go +7 -7
| @@ -334,9 +334,9 @@ func TestAC6CLIEndToEnd(t *testing.T) { | |||
| 334 | 334 | assert.JSONEq(t, `[]`, res.stdout) | |
| 335 | 335 | ||
| 336 | 336 | // version: no token needed. This server runs on http://127.0.0.1, where | |
| 337 | - | // passkeys are not available. Releases are wired by P8; this rig predates it. | |
| 337 | + | // passkeys are not available. | |
| 338 | 338 | res = c.cli(t, "", "version", "--json").ok(t) | |
| 339 | - | assert.Equal(t, 2, strings.Count(res.stderr, "warning:"), res.stderr) | |
| 339 | + | assert.Equal(t, 1, strings.Count(res.stderr, "warning:"), res.stderr) | |
| 340 | 340 | assert.Contains(t, res.stderr, "the server does not support passkeys") | |
| 341 | 341 | type versionOut struct { | |
| 342 | 342 | Client struct{ Version string } | |
| @@ -350,12 +350,12 @@ func TestAC6CLIEndToEnd(t *testing.T) { | |||
| 350 | 350 | ver := decode[versionOut](t, res.stdout) | |
| 351 | 351 | assert.Equal(t, c.url, ver.Server.URL) | |
| 352 | 352 | assert.NotEmpty(t, ver.Server.Version) | |
| 353 | - | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror", "merge_requests.auto_merge", "merge_requests.ignore_checks", "merge_requests.ready"}, ver.Server.Features) | |
| 354 | - | assert.Equal(t, []string{"releases", "passkeys"}, ver.Server.MissingFeatures) | |
| 353 | + | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror", "merge_requests.auto_merge", "merge_requests.ignore_checks", "merge_requests.ready", "releases"}, ver.Server.Features) | |
| 354 | + | assert.Equal(t, []string{"passkeys"}, ver.Server.MissingFeatures) | |
| 355 | 355 | stdout, stderr, code = c.bin(t, secret, "version") | |
| 356 | 356 | require.Equal(t, 0, code, stderr) | |
| 357 | 357 | assert.Contains(t, stdout, "server") | |
| 358 | - | assert.Equal(t, 2, strings.Count(stderr, "warning:"), stderr) | |
| 358 | + | assert.Equal(t, 1, strings.Count(stderr, "warning:"), stderr) | |
| 359 | 359 | assert.Contains(t, stderr, "the server does not support passkeys") | |
| 360 | 360 | ||
| 361 | 361 | // Tokens: create a second one, list, revoke it, see it stop working. | |
| @@ -389,8 +389,8 @@ func TestAC6CLIEndToEnd(t *testing.T) { | |||
| 389 | 389 | assert.Empty(t, tk.Secret, "list never shows secrets") | |
| 390 | 390 | byLabel[tk.Label] = tk | |
| 391 | 391 | } | |
| 392 | - | assert.Len(t, byLabel["daily"].Scopes, len(client.AllScopes())-1, "the cli preset drops release:write until P8 wires releases") | |
| 393 | - | assert.NotContains(t, byLabel["daily"].Scopes, "release:write") | |
| 392 | + | assert.Len(t, byLabel["daily"].Scopes, len(client.AllScopes())) | |
| 393 | + | assert.Contains(t, byLabel["daily"].Scopes, "release:write") | |
| 394 | 394 | assert.Equal(t, tmp.ID, byLabel["tmp"].ID) | |
| 395 | 395 | assert.NotContains(t, res.stdout, `"secret"`) | |
| 396 | 396 | res = c.cli(t, secret, "token", "list").ok(t) | |
| @@ -334,9 +334,9 @@ func TestAC6CLIEndToEnd(t *testing.T) { | |||||
| 334 | assert.JSONEq(t, `[]`, res.stdout) | 334 | assert.JSONEq(t, `[]`, res.stdout) | ||
| 335 | 335 | ||||
| 336 | // version: no token needed. This server runs on http://127.0.0.1, where | 336 | // version: no token needed. This server runs on http://127.0.0.1, where | ||
| 337 | - | // passkeys are not available. Releases are wired by P8; this rig predates it. | 337 | + | // passkeys are not available. |
| 338 | res = c.cli(t, "", "version", "--json").ok(t) | 338 | res = c.cli(t, "", "version", "--json").ok(t) | ||
| 339 | - | assert.Equal(t, 2, strings.Count(res.stderr, "warning:"), res.stderr) | 339 | + | assert.Equal(t, 1, strings.Count(res.stderr, "warning:"), res.stderr) |
| 340 | assert.Contains(t, res.stderr, "the server does not support passkeys") | 340 | assert.Contains(t, res.stderr, "the server does not support passkeys") | ||
| 341 | type versionOut struct { | 341 | type versionOut struct { | ||
| 342 | Client struct{ Version string } | 342 | Client struct{ Version string } | ||
| @@ -350,12 +350,12 @@ func TestAC6CLIEndToEnd(t *testing.T) { | |||||
| 350 | ver := decode[versionOut](t, res.stdout) | 350 | ver := decode[versionOut](t, res.stdout) | ||
| 351 | assert.Equal(t, c.url, ver.Server.URL) | 351 | assert.Equal(t, c.url, ver.Server.URL) | ||
| 352 | assert.NotEmpty(t, ver.Server.Version) | 352 | assert.NotEmpty(t, ver.Server.Version) | ||
| 353 | - | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror", "merge_requests.auto_merge", "merge_requests.ignore_checks", "merge_requests.ready"}, ver.Server.Features) | 353 | + | assert.ElementsMatch(t, []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "issues.links", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.workflows", "ci.cache", "ci.secrets", "ci.artifacts", "commit_signatures", "mirror", "merge_requests.auto_merge", "merge_requests.ignore_checks", "merge_requests.ready", "releases"}, ver.Server.Features) |
| 354 | - | assert.Equal(t, []string{"releases", "passkeys"}, ver.Server.MissingFeatures) | 354 | + | assert.Equal(t, []string{"passkeys"}, ver.Server.MissingFeatures) |
| 355 | stdout, stderr, code = c.bin(t, secret, "version") | 355 | stdout, stderr, code = c.bin(t, secret, "version") | ||
| 356 | require.Equal(t, 0, code, stderr) | 356 | require.Equal(t, 0, code, stderr) | ||
| 357 | assert.Contains(t, stdout, "server") | 357 | assert.Contains(t, stdout, "server") | ||
| 358 | - | assert.Equal(t, 2, strings.Count(stderr, "warning:"), stderr) | 358 | + | assert.Equal(t, 1, strings.Count(stderr, "warning:"), stderr) |
| 359 | assert.Contains(t, stderr, "the server does not support passkeys") | 359 | assert.Contains(t, stderr, "the server does not support passkeys") | ||
| 360 | 360 | ||||
| 361 | // Tokens: create a second one, list, revoke it, see it stop working. | 361 | // Tokens: create a second one, list, revoke it, see it stop working. | ||
| @@ -389,8 +389,8 @@ func TestAC6CLIEndToEnd(t *testing.T) { | |||||
| 389 | assert.Empty(t, tk.Secret, "list never shows secrets") | 389 | assert.Empty(t, tk.Secret, "list never shows secrets") | ||
| 390 | byLabel[tk.Label] = tk | 390 | byLabel[tk.Label] = tk | ||
| 391 | } | 391 | } | ||
| 392 | - | assert.Len(t, byLabel["daily"].Scopes, len(client.AllScopes())-1, "the cli preset drops release:write until P8 wires releases") | 392 | + | assert.Len(t, byLabel["daily"].Scopes, len(client.AllScopes())) |
| 393 | - | assert.NotContains(t, byLabel["daily"].Scopes, "release:write") | 393 | + | assert.Contains(t, byLabel["daily"].Scopes, "release:write") |
| 394 | assert.Equal(t, tmp.ID, byLabel["tmp"].ID) | 394 | assert.Equal(t, tmp.ID, byLabel["tmp"].ID) | ||
| 395 | assert.NotContains(t, res.stdout, `"secret"`) | 395 | assert.NotContains(t, res.stdout, `"secret"`) | ||
| 396 | res = c.cli(t, secret, "token", "list").ok(t) | 396 | res = c.cli(t, secret, "token", "list").ok(t) | ||
M internal/app/cli_release_e2e_test.go +7 -16
| @@ -13,10 +13,7 @@ import ( | |||
| 13 | 13 | "github.com/stretchr/testify/assert" | |
| 14 | 14 | "github.com/stretchr/testify/require" | |
| 15 | 15 | ||
| 16 | - | "bnema.dev/ermite/internal/adapters/blobdisk" | |
| 17 | 16 | "bnema.dev/ermite/internal/adapters/rpc" | |
| 18 | - | "bnema.dev/ermite/internal/ports" | |
| 19 | - | "bnema.dev/ermite/internal/usecase" | |
| 20 | 17 | ) | |
| 21 | 18 | ||
| 22 | 19 | func TestReleaseCLIEndToEnd(t *testing.T) { | |
| @@ -26,17 +23,6 @@ func TestReleaseCLIEndToEnd(t *testing.T) { | |||
| 26 | 23 | c.mustGit(wt, "tag", "v1") | |
| 27 | 24 | c.mustGit(wt, "push", "-q", c.urlAuth(adminToken, "/proj.git"), "main", "refs/tags/v1") | |
| 28 | 25 | ||
| 29 | - | // P8 owns production wiring. Compose the real release service and RPC here, | |
| 30 | - | // sharing the forge's database, auth, git reader, pushes and event bus. | |
| 31 | - | f := c.forge | |
| 32 | - | releases := usecase.NewReleases(f.store.Releases(), blobdisk.New(filepath.Join(c.data, "release-test-blobs")), f.store, f.git, f.git, f.Pushes, f.Bus, systemClock{}, ports.ReleaseLimits{}) | |
| 33 | - | api := rpc.New(rpc.Config{Auth: f.Auth, Releases: releases, SigningKey: f.SigningKey, Sessions: f.Sessions}) | |
| 34 | - | mux := http.NewServeMux() | |
| 35 | - | mux.Handle("/-/api/", http.StripPrefix("/-/api", api.Handler())) | |
| 36 | - | srv := httptest.NewServer(mux) | |
| 37 | - | t.Cleanup(srv.Close) | |
| 38 | - | oldURL := c.url | |
| 39 | - | c.url = srv.URL | |
| 40 | 26 | bin := func(args ...string) string { | |
| 41 | 27 | t.Helper() | |
| 42 | 28 | out, stderr, code := c.bin(t, adminToken, args...) | |
| @@ -67,8 +53,13 @@ func TestReleaseCLIEndToEnd(t *testing.T) { | |||
| 67 | 53 | bin("release", "delete", "proj", "v1", "--yes") | |
| 68 | 54 | _, stderr, code = c.bin(t, adminToken, "release", "view", "proj", "v1") | |
| 69 | 55 | requireOneLineError(t, stderr, code, "not found") | |
| 70 | - | // The unmodified forge intentionally has no release wiring yet. | |
| 71 | - | c.url = oldURL | |
| 56 | + | // An older server without the service still gets a clear refusal. | |
| 57 | + | api := rpc.New(rpc.Config{Auth: c.forge.Auth, SigningKey: c.forge.SigningKey, Sessions: c.forge.Sessions}) | |
| 58 | + | mux := http.NewServeMux() | |
| 59 | + | mux.Handle("/-/api/", http.StripPrefix("/-/api", api.Handler())) | |
| 60 | + | srv := httptest.NewServer(mux) | |
| 61 | + | t.Cleanup(srv.Close) | |
| 62 | + | c.url = srv.URL | |
| 72 | 63 | _, stderr, code = c.bin(t, adminToken, "release", "list", "proj") | |
| 73 | 64 | requireOneLineError(t, stderr, code, "the server has no releases (it lacks the releases feature)") | |
| 74 | 65 | } | |
| @@ -13,10 +13,7 @@ import ( | |||||
| 13 | "github.com/stretchr/testify/assert" | 13 | "github.com/stretchr/testify/assert" | ||
| 14 | "github.com/stretchr/testify/require" | 14 | "github.com/stretchr/testify/require" | ||
| 15 | 15 | ||||
| 16 | - | "bnema.dev/ermite/internal/adapters/blobdisk" | |||
| 17 | "bnema.dev/ermite/internal/adapters/rpc" | 16 | "bnema.dev/ermite/internal/adapters/rpc" | ||
| 18 | - | "bnema.dev/ermite/internal/ports" | |||
| 19 | - | "bnema.dev/ermite/internal/usecase" | |||
| 20 | ) | 17 | ) | ||
| 21 | 18 | ||||
| 22 | func TestReleaseCLIEndToEnd(t *testing.T) { | 19 | func TestReleaseCLIEndToEnd(t *testing.T) { | ||
| @@ -26,17 +23,6 @@ func TestReleaseCLIEndToEnd(t *testing.T) { | |||||
| 26 | c.mustGit(wt, "tag", "v1") | 23 | c.mustGit(wt, "tag", "v1") | ||
| 27 | c.mustGit(wt, "push", "-q", c.urlAuth(adminToken, "/proj.git"), "main", "refs/tags/v1") | 24 | c.mustGit(wt, "push", "-q", c.urlAuth(adminToken, "/proj.git"), "main", "refs/tags/v1") | ||
| 28 | 25 | ||||
| 29 | - | // P8 owns production wiring. Compose the real release service and RPC here, | |||
| 30 | - | // sharing the forge's database, auth, git reader, pushes and event bus. | |||
| 31 | - | f := c.forge | |||
| 32 | - | releases := usecase.NewReleases(f.store.Releases(), blobdisk.New(filepath.Join(c.data, "release-test-blobs")), f.store, f.git, f.git, f.Pushes, f.Bus, systemClock{}, ports.ReleaseLimits{}) | |||
| 33 | - | api := rpc.New(rpc.Config{Auth: f.Auth, Releases: releases, SigningKey: f.SigningKey, Sessions: f.Sessions}) | |||
| 34 | - | mux := http.NewServeMux() | |||
| 35 | - | mux.Handle("/-/api/", http.StripPrefix("/-/api", api.Handler())) | |||
| 36 | - | srv := httptest.NewServer(mux) | |||
| 37 | - | t.Cleanup(srv.Close) | |||
| 38 | - | oldURL := c.url | |||
| 39 | - | c.url = srv.URL | |||
| 40 | bin := func(args ...string) string { | 26 | bin := func(args ...string) string { | ||
| 41 | t.Helper() | 27 | t.Helper() | ||
| 42 | out, stderr, code := c.bin(t, adminToken, args...) | 28 | out, stderr, code := c.bin(t, adminToken, args...) | ||
| @@ -67,8 +53,13 @@ func TestReleaseCLIEndToEnd(t *testing.T) { | |||||
| 67 | bin("release", "delete", "proj", "v1", "--yes") | 53 | bin("release", "delete", "proj", "v1", "--yes") | ||
| 68 | _, stderr, code = c.bin(t, adminToken, "release", "view", "proj", "v1") | 54 | _, stderr, code = c.bin(t, adminToken, "release", "view", "proj", "v1") | ||
| 69 | requireOneLineError(t, stderr, code, "not found") | 55 | requireOneLineError(t, stderr, code, "not found") | ||
| 70 | - | // The unmodified forge intentionally has no release wiring yet. | 56 | + | // An older server without the service still gets a clear refusal. |
| 71 | - | c.url = oldURL | 57 | + | api := rpc.New(rpc.Config{Auth: c.forge.Auth, SigningKey: c.forge.SigningKey, Sessions: c.forge.Sessions}) |
| 58 | + | mux := http.NewServeMux() | |||
| 59 | + | mux.Handle("/-/api/", http.StripPrefix("/-/api", api.Handler())) | |||
| 60 | + | srv := httptest.NewServer(mux) | |||
| 61 | + | t.Cleanup(srv.Close) | |||
| 62 | + | c.url = srv.URL | |||
| 72 | _, stderr, code = c.bin(t, adminToken, "release", "list", "proj") | 63 | _, stderr, code = c.bin(t, adminToken, "release", "list", "proj") | ||
| 73 | requireOneLineError(t, stderr, code, "the server has no releases (it lacks the releases feature)") | 64 | requireOneLineError(t, stderr, code, "the server has no releases (it lacks the releases feature)") | ||
| 74 | } | 65 | } | ||
M internal/app/cli_version_test.go +5 -5
| @@ -17,7 +17,7 @@ import ( | |||
| 17 | 17 | ) | |
| 18 | 18 | ||
| 19 | 19 | // allFeatures are the features this client uses. | |
| 20 | - | var allFeatures = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "merge_requests.mergeability", "signing_key", "session", "token.branch_prefixes", "ci.runs", "ci.runners", "ci.secrets", "ci.dispatch", "ci.workflows", "ci.artifacts", "commit_signatures", "merge_requests.auto_merge", "passkeys"} | |
| 20 | + | var allFeatures = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "merge_requests.mergeability", "signing_key", "session", "token.branch_prefixes", "ci.runs", "ci.runners", "ci.secrets", "ci.dispatch", "ci.workflows", "ci.artifacts", "commit_signatures", "merge_requests.auto_merge", "releases", "passkeys"} | |
| 21 | 21 | ||
| 22 | 22 | // Every feature the client uses needs a sentence for the version warning. | |
| 23 | 23 | func TestEveryUsedFeatureExplainsWhatBreaks(t *testing.T) { | |
| @@ -45,15 +45,15 @@ func TestVersionWithServer(t *testing.T) { | |||
| 45 | 45 | f := versionFake(allFeatures...) | |
| 46 | 46 | url := f.serve(t) | |
| 47 | 47 | res := runCLI(t, envFor(url), "", "version").ok(t) | |
| 48 | - | assert.Equal(t, "client ermite v0.0.1\nserver v0.4.0 at "+url+"\nfeatures repo.update, events.resume, merge_requests, merge_requests.delete_branch, issues, merge_requests.mergeability, signing_key, session, token.branch_prefixes, ci.runs, ci.runners, ci.secrets, ci.dispatch, ci.workflows, ci.artifacts, commit_signatures, merge_requests.auto_merge, passkeys\n", res.stdout) | |
| 48 | + | assert.Equal(t, "client ermite v0.0.1\nserver v0.4.0 at "+url+"\nfeatures repo.update, events.resume, merge_requests, merge_requests.delete_branch, issues, merge_requests.mergeability, signing_key, session, token.branch_prefixes, ci.runs, ci.runners, ci.secrets, ci.dispatch, ci.workflows, ci.artifacts, commit_signatures, merge_requests.auto_merge, releases, passkeys\n", res.stdout) | |
| 49 | 49 | assert.Empty(t, res.stderr, "no warning when every feature is there") | |
| 50 | 50 | ||
| 51 | 51 | res = runCLI(t, envFor(url), "", "version", "--json").ok(t) | |
| 52 | - | assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","events.resume","merge_requests","merge_requests.delete_branch","issues","merge_requests.mergeability","signing_key","session","token.branch_prefixes","ci.runs","ci.runners","ci.secrets","ci.dispatch","ci.workflows","ci.artifacts","commit_signatures","merge_requests.auto_merge","passkeys"],"missing_features":[]}}`, res.stdout) | |
| 52 | + | assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","events.resume","merge_requests","merge_requests.delete_branch","issues","merge_requests.mergeability","signing_key","session","token.branch_prefixes","ci.runs","ci.runners","ci.secrets","ci.dispatch","ci.workflows","ci.artifacts","commit_signatures","merge_requests.auto_merge","releases","passkeys"],"missing_features":[]}}`, res.stdout) | |
| 53 | 53 | } | |
| 54 | 54 | ||
| 55 | 55 | func TestVersionWarnsAboutMissingFeatures(t *testing.T) { | |
| 56 | - | f := versionFake("repo.update", "merge_requests", "merge_requests.delete_branch", "issues", "merge_requests.mergeability", "signing_key", "session", "token.branch_prefixes", "ci.runs", "ci.runners", "ci.secrets", "ci.dispatch", "ci.workflows", "ci.artifacts", "commit_signatures", "merge_requests.auto_merge", "passkeys") | |
| 56 | + | f := versionFake("repo.update", "merge_requests", "merge_requests.delete_branch", "issues", "merge_requests.mergeability", "signing_key", "session", "token.branch_prefixes", "ci.runs", "ci.runners", "ci.secrets", "ci.dispatch", "ci.workflows", "ci.artifacts", "commit_signatures", "merge_requests.auto_merge", "releases", "passkeys") | |
| 57 | 57 | url := f.serve(t) | |
| 58 | 58 | res := runCLI(t, envFor(url), "", "version").ok(t) | |
| 59 | 59 | assert.Contains(t, res.stdout, "v0.4.0") | |
| @@ -61,7 +61,7 @@ func TestVersionWarnsAboutMissingFeatures(t *testing.T) { | |||
| 61 | 61 | assert.NotContains(t, res.stderr, "repo.update") | |
| 62 | 62 | ||
| 63 | 63 | res = runCLI(t, envFor(url), "", "version", "--json").ok(t) | |
| 64 | - | assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","merge_requests","merge_requests.delete_branch","issues","merge_requests.mergeability","signing_key","session","token.branch_prefixes","ci.runs","ci.runners","ci.secrets","ci.dispatch","ci.workflows","ci.artifacts","commit_signatures","merge_requests.auto_merge","passkeys"],"missing_features":["events.resume"]}}`, res.stdout) | |
| 64 | + | assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","merge_requests","merge_requests.delete_branch","issues","merge_requests.mergeability","signing_key","session","token.branch_prefixes","ci.runs","ci.runners","ci.secrets","ci.dispatch","ci.workflows","ci.artifacts","commit_signatures","merge_requests.auto_merge","releases","passkeys"],"missing_features":["events.resume"]}}`, res.stdout) | |
| 65 | 65 | assert.Contains(t, res.stderr, "events.resume", "the warning also goes to stderr with --json") | |
| 66 | 66 | ||
| 67 | 67 | f = versionFake() | |
| @@ -17,7 +17,7 @@ import ( | |||||
| 17 | ) | 17 | ) | ||
| 18 | 18 | ||||
| 19 | // allFeatures are the features this client uses. | 19 | // allFeatures are the features this client uses. | ||
| 20 | - | var allFeatures = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "merge_requests.mergeability", "signing_key", "session", "token.branch_prefixes", "ci.runs", "ci.runners", "ci.secrets", "ci.dispatch", "ci.workflows", "ci.artifacts", "commit_signatures", "merge_requests.auto_merge", "passkeys"} | 20 | + | var allFeatures = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "merge_requests.mergeability", "signing_key", "session", "token.branch_prefixes", "ci.runs", "ci.runners", "ci.secrets", "ci.dispatch", "ci.workflows", "ci.artifacts", "commit_signatures", "merge_requests.auto_merge", "releases", "passkeys"} |
| 21 | 21 | ||||
| 22 | // Every feature the client uses needs a sentence for the version warning. | 22 | // Every feature the client uses needs a sentence for the version warning. | ||
| 23 | func TestEveryUsedFeatureExplainsWhatBreaks(t *testing.T) { | 23 | func TestEveryUsedFeatureExplainsWhatBreaks(t *testing.T) { | ||
| @@ -45,15 +45,15 @@ func TestVersionWithServer(t *testing.T) { | |||||
| 45 | f := versionFake(allFeatures...) | 45 | f := versionFake(allFeatures...) | ||
| 46 | url := f.serve(t) | 46 | url := f.serve(t) | ||
| 47 | res := runCLI(t, envFor(url), "", "version").ok(t) | 47 | res := runCLI(t, envFor(url), "", "version").ok(t) | ||
| 48 | - | assert.Equal(t, "client ermite v0.0.1\nserver v0.4.0 at "+url+"\nfeatures repo.update, events.resume, merge_requests, merge_requests.delete_branch, issues, merge_requests.mergeability, signing_key, session, token.branch_prefixes, ci.runs, ci.runners, ci.secrets, ci.dispatch, ci.workflows, ci.artifacts, commit_signatures, merge_requests.auto_merge, passkeys\n", res.stdout) | 48 | + | assert.Equal(t, "client ermite v0.0.1\nserver v0.4.0 at "+url+"\nfeatures repo.update, events.resume, merge_requests, merge_requests.delete_branch, issues, merge_requests.mergeability, signing_key, session, token.branch_prefixes, ci.runs, ci.runners, ci.secrets, ci.dispatch, ci.workflows, ci.artifacts, commit_signatures, merge_requests.auto_merge, releases, passkeys\n", res.stdout) |
| 49 | assert.Empty(t, res.stderr, "no warning when every feature is there") | 49 | assert.Empty(t, res.stderr, "no warning when every feature is there") | ||
| 50 | 50 | ||||
| 51 | res = runCLI(t, envFor(url), "", "version", "--json").ok(t) | 51 | res = runCLI(t, envFor(url), "", "version", "--json").ok(t) | ||
| 52 | - | assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","events.resume","merge_requests","merge_requests.delete_branch","issues","merge_requests.mergeability","signing_key","session","token.branch_prefixes","ci.runs","ci.runners","ci.secrets","ci.dispatch","ci.workflows","ci.artifacts","commit_signatures","merge_requests.auto_merge","passkeys"],"missing_features":[]}}`, res.stdout) | 52 | + | assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","events.resume","merge_requests","merge_requests.delete_branch","issues","merge_requests.mergeability","signing_key","session","token.branch_prefixes","ci.runs","ci.runners","ci.secrets","ci.dispatch","ci.workflows","ci.artifacts","commit_signatures","merge_requests.auto_merge","releases","passkeys"],"missing_features":[]}}`, res.stdout) |
| 53 | } | 53 | } | ||
| 54 | 54 | ||||
| 55 | func TestVersionWarnsAboutMissingFeatures(t *testing.T) { | 55 | func TestVersionWarnsAboutMissingFeatures(t *testing.T) { | ||
| 56 | - | f := versionFake("repo.update", "merge_requests", "merge_requests.delete_branch", "issues", "merge_requests.mergeability", "signing_key", "session", "token.branch_prefixes", "ci.runs", "ci.runners", "ci.secrets", "ci.dispatch", "ci.workflows", "ci.artifacts", "commit_signatures", "merge_requests.auto_merge", "passkeys") | 56 | + | f := versionFake("repo.update", "merge_requests", "merge_requests.delete_branch", "issues", "merge_requests.mergeability", "signing_key", "session", "token.branch_prefixes", "ci.runs", "ci.runners", "ci.secrets", "ci.dispatch", "ci.workflows", "ci.artifacts", "commit_signatures", "merge_requests.auto_merge", "releases", "passkeys") |
| 57 | url := f.serve(t) | 57 | url := f.serve(t) | ||
| 58 | res := runCLI(t, envFor(url), "", "version").ok(t) | 58 | res := runCLI(t, envFor(url), "", "version").ok(t) | ||
| 59 | assert.Contains(t, res.stdout, "v0.4.0") | 59 | assert.Contains(t, res.stdout, "v0.4.0") | ||
| @@ -61,7 +61,7 @@ func TestVersionWarnsAboutMissingFeatures(t *testing.T) { | |||||
| 61 | assert.NotContains(t, res.stderr, "repo.update") | 61 | assert.NotContains(t, res.stderr, "repo.update") | ||
| 62 | 62 | ||||
| 63 | res = runCLI(t, envFor(url), "", "version", "--json").ok(t) | 63 | res = runCLI(t, envFor(url), "", "version", "--json").ok(t) | ||
| 64 | - | assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","merge_requests","merge_requests.delete_branch","issues","merge_requests.mergeability","signing_key","session","token.branch_prefixes","ci.runs","ci.runners","ci.secrets","ci.dispatch","ci.workflows","ci.artifacts","commit_signatures","merge_requests.auto_merge","passkeys"],"missing_features":["events.resume"]}}`, res.stdout) | 64 | + | assert.JSONEq(t, `{"client":{"version":"v0.0.1"},"server":{"url":"`+url+`","version":"v0.4.0","features":["repo.update","merge_requests","merge_requests.delete_branch","issues","merge_requests.mergeability","signing_key","session","token.branch_prefixes","ci.runs","ci.runners","ci.secrets","ci.dispatch","ci.workflows","ci.artifacts","commit_signatures","merge_requests.auto_merge","releases","passkeys"],"missing_features":["events.resume"]}}`, res.stdout) |
| 65 | assert.Contains(t, res.stderr, "events.resume", "the warning also goes to stderr with --json") | 65 | assert.Contains(t, res.stderr, "events.resume", "the warning also goes to stderr with --json") | ||
| 66 | 66 | ||||
| 67 | f = versionFake() | 67 | f = versionFake() | ||
M internal/app/forge.go +49 -4
| @@ -17,6 +17,8 @@ import ( | |||
| 17 | 17 | ||
| 18 | 18 | "github.com/bnema/zerowrap" | |
| 19 | 19 | ||
| 20 | + | "bnema.dev/ermite/internal/adapters/blobdisk" | |
| 21 | + | "bnema.dev/ermite/internal/adapters/blobs3" | |
| 20 | 22 | "bnema.dev/ermite/internal/adapters/ciartifacts" | |
| 21 | 23 | "bnema.dev/ermite/internal/adapters/cilogs" | |
| 22 | 24 | "bnema.dev/ermite/internal/adapters/cisummaries" | |
| @@ -153,7 +155,9 @@ type forge struct { | |||
| 153 | 155 | EventLog *usecase.Events | |
| 154 | 156 | // CI is the CI engine: workflow trigger, planner, runner protocol and the | |
| 155 | 157 | // owner side. It starts with the server and stops with it. | |
| 156 | - | CI *usecase.CI | |
| 158 | + | CI *usecase.CI | |
| 159 | + | Releases *usecase.Releases | |
| 160 | + | Blobs ports.BlobStore | |
| 157 | 161 | // Settings are the secrets, variables and environments of CI. | |
| 158 | 162 | Settings *usecase.CISettings | |
| 159 | 163 | // Mirrors exposes Warning and Badge for the hook server and web composition. | |
| @@ -268,6 +272,13 @@ func newForge(ctx context.Context, cfg config.Config, opts options) (_ *forge, e | |||
| 268 | 272 | } | |
| 269 | 273 | log.Info().Str("reader", cfg.GitReader).Msg("git reader selected") | |
| 270 | 274 | f.Browse = usecase.NewBrowse(store, reader) | |
| 275 | + | f.Blobs, err = blobStore(ctx, cfg) | |
| 276 | + | if err != nil { | |
| 277 | + | return nil, err | |
| 278 | + | } | |
| 279 | + | f.Releases = usecase.NewReleases(store.Releases(), f.Blobs, store, reader, git, f.Pushes, f.Bus, clock, ports.ReleaseLimits{AssetMax: cfg.ReleaseAssetMax, AssetsMax: int64(cfg.ReleaseAssetsMax), Quota: cfg.ReleaseQuota}) | |
| 280 | + | f.Pushes.WithReleases(f.Releases) | |
| 281 | + | f.Repos.WithReleases(f.Releases) | |
| 271 | 282 | ||
| 272 | 283 | // Secrets, variables and environments (ADR 0019, CI-3). With a key the | |
| 273 | 284 | // secrets are sealed under it, and the server refuses to start when the | |
| @@ -335,6 +346,8 @@ func newForge(ctx context.Context, cfg config.Config, opts options) (_ *forge, e | |||
| 335 | 346 | OIDC: f.OIDC, | |
| 336 | 347 | } | |
| 337 | 348 | artifacts(&ciCfg, cfg, store) | |
| 349 | + | ciCfg.Blobs = f.Blobs | |
| 350 | + | ciCfg.ReleaseSweeper = f.Releases | |
| 338 | 351 | f.pullRequests(&ciCfg, store, git, committer) | |
| 339 | 352 | if opts.ci != nil { | |
| 340 | 353 | opts.ci(&ciCfg) | |
| @@ -459,6 +472,7 @@ func newForge(ctx context.Context, cfg config.Config, opts options) (_ *forge, e | |||
| 459 | 472 | OIDCKeys: f.OIDC, | |
| 460 | 473 | JobArtifacts: jobArtifacts, | |
| 461 | 474 | Artifacts: artifacts, | |
| 475 | + | Releases: f.Releases, | |
| 462 | 476 | Version: serverVersion(), | |
| 463 | 477 | TrustedProxies: cfg.TrustedProxies, | |
| 464 | 478 | Limiter: limiter, | |
| @@ -487,7 +501,8 @@ func newForge(ctx context.Context, cfg config.Config, opts options) (_ *forge, e | |||
| 487 | 501 | routes = append(routes, httpserver.Route{Pattern: p, Handler: gh}) | |
| 488 | 502 | } | |
| 489 | 503 | // The REST subset the actions call on github.api_url. | |
| 490 | - | api := githubapi.New(githubapi.Config{Auth: f.Auth, API: f.CI, TrustedProxies: cfg.TrustedProxies, Limiter: limiter, MaxArchivesPerClient: opts.archivesPerClient}) | |
| 504 | + | releaseAPI := usecase.NewReleasesGitHub(f.Releases, cfg.Owner, cfg.PublicURL) | |
| 505 | + | api := githubapi.New(githubapi.Config{Auth: f.Auth, API: f.CI, Releases: releaseAPI, ReleaseDownload: f.Releases, TrustedProxies: cfg.TrustedProxies, Limiter: limiter, MaxArchivesPerClient: opts.archivesPerClient}) | |
| 491 | 506 | for _, p := range githubapi.Patterns { | |
| 492 | 507 | routes = append(routes, httpserver.Route{Pattern: p, Handler: api}) | |
| 493 | 508 | } | |
| @@ -593,16 +608,46 @@ func (f *forge) Close(ctx context.Context) { | |||
| 593 | 608 | } | |
| 594 | 609 | ||
| 595 | 610 | // artifacts gives the CI the stores and limits of the artifacts (CI-4 D3): | |
| 596 | - | // records in the kv store, contents under ci/artifacts. ERMITE_CI_ARTIFACTS=0 | |
| 611 | + | // records in the kv store, legacy contents under ci/artifacts. ERMITE_CI_ARTIFACTS=0 | |
| 597 | 612 | // gives none, and the API then serves no artifact service. | |
| 598 | 613 | func artifacts(ci *usecase.CIConfig, cfg config.Config, store *kvstore.Store) { | |
| 599 | 614 | if !cfg.CIArtifacts { | |
| 600 | 615 | return | |
| 601 | 616 | } | |
| 602 | - | ci.Artifacts, ci.ArtifactFiles = store.Artifacts(), ciartifacts.New(ciartifacts.Dir(cfg.DataDir)) | |
| 617 | + | ci.Artifacts = store.Artifacts() | |
| 618 | + | if info, err := os.Stat(ciartifacts.Dir(cfg.DataDir)); err == nil && info.IsDir() { | |
| 619 | + | ci.ArtifactFiles = ciartifacts.New(ciartifacts.Dir(cfg.DataDir)) | |
| 620 | + | } | |
| 603 | 621 | ci.ArtifactLimits = ports.ArtifactLimits{Retention: cfg.CIArtifactRetention, Quota: cfg.CIArtifactQuota, MaxSize: cfg.CIArtifactMax} | |
| 604 | 622 | } | |
| 605 | 623 | ||
| 624 | + | // blobStore selects and probes the content backend before any engine starts. | |
| 625 | + | func blobStore(ctx context.Context, cfg config.Config) (ports.BlobStore, error) { | |
| 626 | + | var blobs ports.BlobStore | |
| 627 | + | backend, location := "disk", blobdisk.Dir(cfg.DataDir) | |
| 628 | + | if cfg.S3Bucket == "" { | |
| 629 | + | blobs = blobdisk.New(location) | |
| 630 | + | } else { | |
| 631 | + | backend, location = "s3", cfg.S3Bucket | |
| 632 | + | store, err := blobs3.New(ctx, blobs3.Config{Bucket: cfg.S3Bucket, Prefix: cfg.S3Prefix, Endpoint: cfg.S3Endpoint, Region: cfg.S3Region, AccessKeyID: cfg.S3AccessKeyID, SecretAccessKey: cfg.S3SecretAccessKey, ForcePathStyle: cfg.S3ForcePathStyle}) | |
| 633 | + | if err != nil { | |
| 634 | + | return nil, fmt.Errorf("%s: %w", config.EnvS3Bucket, err) | |
| 635 | + | } | |
| 636 | + | blobs = store | |
| 637 | + | } | |
| 638 | + | probe, cancel := context.WithTimeout(ctx, 30*time.Second) | |
| 639 | + | defer cancel() | |
| 640 | + | if err := blobs.Check(probe); err != nil { | |
| 641 | + | if backend == "s3" { | |
| 642 | + | return nil, fmt.Errorf("%s: %w", config.EnvS3Bucket, err) | |
| 643 | + | } | |
| 644 | + | return nil, fmt.Errorf("blob store: %w", err) | |
| 645 | + | } | |
| 646 | + | log := zerowrap.FromCtx(ctx) | |
| 647 | + | log.Info().Str("backend", backend).Str("location", location).Msg("blob store ready") | |
| 648 | + | return blobs, nil | |
| 649 | + | } | |
| 650 | + | ||
| 606 | 651 | // pullRequests gives the CI the merge requests, the refs/pull refs and the | |
| 607 | 652 | // identity of the test merge commits (CI-3, D5). | |
| 608 | 653 | func (f *forge) pullRequests(cfg *usecase.CIConfig, store *kvstore.Store, git *gitcli.Git, committer domain.Identity) { | |
| @@ -17,6 +17,8 @@ import ( | |||||
| 17 | 17 | ||||
| 18 | "github.com/bnema/zerowrap" | 18 | "github.com/bnema/zerowrap" | ||
| 19 | 19 | ||||
| 20 | + | "bnema.dev/ermite/internal/adapters/blobdisk" | |||
| 21 | + | "bnema.dev/ermite/internal/adapters/blobs3" | |||
| 20 | "bnema.dev/ermite/internal/adapters/ciartifacts" | 22 | "bnema.dev/ermite/internal/adapters/ciartifacts" | ||
| 21 | "bnema.dev/ermite/internal/adapters/cilogs" | 23 | "bnema.dev/ermite/internal/adapters/cilogs" | ||
| 22 | "bnema.dev/ermite/internal/adapters/cisummaries" | 24 | "bnema.dev/ermite/internal/adapters/cisummaries" | ||
| @@ -153,7 +155,9 @@ type forge struct { | |||||
| 153 | EventLog *usecase.Events | 155 | EventLog *usecase.Events | ||
| 154 | // CI is the CI engine: workflow trigger, planner, runner protocol and the | 156 | // CI is the CI engine: workflow trigger, planner, runner protocol and the | ||
| 155 | // owner side. It starts with the server and stops with it. | 157 | // owner side. It starts with the server and stops with it. | ||
| 156 | - | CI *usecase.CI | 158 | + | CI *usecase.CI |
| 159 | + | Releases *usecase.Releases | |||
| 160 | + | Blobs ports.BlobStore | |||
| 157 | // Settings are the secrets, variables and environments of CI. | 161 | // Settings are the secrets, variables and environments of CI. | ||
| 158 | Settings *usecase.CISettings | 162 | Settings *usecase.CISettings | ||
| 159 | // Mirrors exposes Warning and Badge for the hook server and web composition. | 163 | // Mirrors exposes Warning and Badge for the hook server and web composition. | ||
| @@ -268,6 +272,13 @@ func newForge(ctx context.Context, cfg config.Config, opts options) (_ *forge, e | |||||
| 268 | } | 272 | } | ||
| 269 | log.Info().Str("reader", cfg.GitReader).Msg("git reader selected") | 273 | log.Info().Str("reader", cfg.GitReader).Msg("git reader selected") | ||
| 270 | f.Browse = usecase.NewBrowse(store, reader) | 274 | f.Browse = usecase.NewBrowse(store, reader) | ||
| 275 | + | f.Blobs, err = blobStore(ctx, cfg) | |||
| 276 | + | if err != nil { | |||
| 277 | + | return nil, err | |||
| 278 | + | } | |||
| 279 | + | f.Releases = usecase.NewReleases(store.Releases(), f.Blobs, store, reader, git, f.Pushes, f.Bus, clock, ports.ReleaseLimits{AssetMax: cfg.ReleaseAssetMax, AssetsMax: int64(cfg.ReleaseAssetsMax), Quota: cfg.ReleaseQuota}) | |||
| 280 | + | f.Pushes.WithReleases(f.Releases) | |||
| 281 | + | f.Repos.WithReleases(f.Releases) | |||
| 271 | 282 | ||||
| 272 | // Secrets, variables and environments (ADR 0019, CI-3). With a key the | 283 | // Secrets, variables and environments (ADR 0019, CI-3). With a key the | ||
| 273 | // secrets are sealed under it, and the server refuses to start when the | 284 | // secrets are sealed under it, and the server refuses to start when the | ||
| @@ -335,6 +346,8 @@ func newForge(ctx context.Context, cfg config.Config, opts options) (_ *forge, e | |||||
| 335 | OIDC: f.OIDC, | 346 | OIDC: f.OIDC, | ||
| 336 | } | 347 | } | ||
| 337 | artifacts(&ciCfg, cfg, store) | 348 | artifacts(&ciCfg, cfg, store) | ||
| 349 | + | ciCfg.Blobs = f.Blobs | |||
| 350 | + | ciCfg.ReleaseSweeper = f.Releases | |||
| 338 | f.pullRequests(&ciCfg, store, git, committer) | 351 | f.pullRequests(&ciCfg, store, git, committer) | ||
| 339 | if opts.ci != nil { | 352 | if opts.ci != nil { | ||
| 340 | opts.ci(&ciCfg) | 353 | opts.ci(&ciCfg) | ||
| @@ -459,6 +472,7 @@ func newForge(ctx context.Context, cfg config.Config, opts options) (_ *forge, e | |||||
| 459 | OIDCKeys: f.OIDC, | 472 | OIDCKeys: f.OIDC, | ||
| 460 | JobArtifacts: jobArtifacts, | 473 | JobArtifacts: jobArtifacts, | ||
| 461 | Artifacts: artifacts, | 474 | Artifacts: artifacts, | ||
| 475 | + | Releases: f.Releases, | |||
| 462 | Version: serverVersion(), | 476 | Version: serverVersion(), | ||
| 463 | TrustedProxies: cfg.TrustedProxies, | 477 | TrustedProxies: cfg.TrustedProxies, | ||
| 464 | Limiter: limiter, | 478 | Limiter: limiter, | ||
| @@ -487,7 +501,8 @@ func newForge(ctx context.Context, cfg config.Config, opts options) (_ *forge, e | |||||
| 487 | routes = append(routes, httpserver.Route{Pattern: p, Handler: gh}) | 501 | routes = append(routes, httpserver.Route{Pattern: p, Handler: gh}) | ||
| 488 | } | 502 | } | ||
| 489 | // The REST subset the actions call on github.api_url. | 503 | // The REST subset the actions call on github.api_url. | ||
| 490 | - | api := githubapi.New(githubapi.Config{Auth: f.Auth, API: f.CI, TrustedProxies: cfg.TrustedProxies, Limiter: limiter, MaxArchivesPerClient: opts.archivesPerClient}) | 504 | + | releaseAPI := usecase.NewReleasesGitHub(f.Releases, cfg.Owner, cfg.PublicURL) |
| 505 | + | api := githubapi.New(githubapi.Config{Auth: f.Auth, API: f.CI, Releases: releaseAPI, ReleaseDownload: f.Releases, TrustedProxies: cfg.TrustedProxies, Limiter: limiter, MaxArchivesPerClient: opts.archivesPerClient}) | |||
| 491 | for _, p := range githubapi.Patterns { | 506 | for _, p := range githubapi.Patterns { | ||
| 492 | routes = append(routes, httpserver.Route{Pattern: p, Handler: api}) | 507 | routes = append(routes, httpserver.Route{Pattern: p, Handler: api}) | ||
| 493 | } | 508 | } | ||
| @@ -593,16 +608,46 @@ func (f *forge) Close(ctx context.Context) { | |||||
| 593 | } | 608 | } | ||
| 594 | 609 | ||||
| 595 | // artifacts gives the CI the stores and limits of the artifacts (CI-4 D3): | 610 | // artifacts gives the CI the stores and limits of the artifacts (CI-4 D3): | ||
| 596 | - | // records in the kv store, contents under ci/artifacts. ERMITE_CI_ARTIFACTS=0 | 611 | + | // records in the kv store, legacy contents under ci/artifacts. ERMITE_CI_ARTIFACTS=0 |
| 597 | // gives none, and the API then serves no artifact service. | 612 | // gives none, and the API then serves no artifact service. | ||
| 598 | func artifacts(ci *usecase.CIConfig, cfg config.Config, store *kvstore.Store) { | 613 | func artifacts(ci *usecase.CIConfig, cfg config.Config, store *kvstore.Store) { | ||
| 599 | if !cfg.CIArtifacts { | 614 | if !cfg.CIArtifacts { | ||
| 600 | return | 615 | return | ||
| 601 | } | 616 | } | ||
| 602 | - | ci.Artifacts, ci.ArtifactFiles = store.Artifacts(), ciartifacts.New(ciartifacts.Dir(cfg.DataDir)) | 617 | + | ci.Artifacts = store.Artifacts() |
| 618 | + | if info, err := os.Stat(ciartifacts.Dir(cfg.DataDir)); err == nil && info.IsDir() { | |||
| 619 | + | ci.ArtifactFiles = ciartifacts.New(ciartifacts.Dir(cfg.DataDir)) | |||
| 620 | + | } | |||
| 603 | ci.ArtifactLimits = ports.ArtifactLimits{Retention: cfg.CIArtifactRetention, Quota: cfg.CIArtifactQuota, MaxSize: cfg.CIArtifactMax} | 621 | ci.ArtifactLimits = ports.ArtifactLimits{Retention: cfg.CIArtifactRetention, Quota: cfg.CIArtifactQuota, MaxSize: cfg.CIArtifactMax} | ||
| 604 | } | 622 | } | ||
| 605 | 623 | ||||
| 624 | + | // blobStore selects and probes the content backend before any engine starts. | |||
| 625 | + | func blobStore(ctx context.Context, cfg config.Config) (ports.BlobStore, error) { | |||
| 626 | + | var blobs ports.BlobStore | |||
| 627 | + | backend, location := "disk", blobdisk.Dir(cfg.DataDir) | |||
| 628 | + | if cfg.S3Bucket == "" { | |||
| 629 | + | blobs = blobdisk.New(location) | |||
| 630 | + | } else { | |||
| 631 | + | backend, location = "s3", cfg.S3Bucket | |||
| 632 | + | store, err := blobs3.New(ctx, blobs3.Config{Bucket: cfg.S3Bucket, Prefix: cfg.S3Prefix, Endpoint: cfg.S3Endpoint, Region: cfg.S3Region, AccessKeyID: cfg.S3AccessKeyID, SecretAccessKey: cfg.S3SecretAccessKey, ForcePathStyle: cfg.S3ForcePathStyle}) | |||
| 633 | + | if err != nil { | |||
| 634 | + | return nil, fmt.Errorf("%s: %w", config.EnvS3Bucket, err) | |||
| 635 | + | } | |||
| 636 | + | blobs = store | |||
| 637 | + | } | |||
| 638 | + | probe, cancel := context.WithTimeout(ctx, 30*time.Second) | |||
| 639 | + | defer cancel() | |||
| 640 | + | if err := blobs.Check(probe); err != nil { | |||
| 641 | + | if backend == "s3" { | |||
| 642 | + | return nil, fmt.Errorf("%s: %w", config.EnvS3Bucket, err) | |||
| 643 | + | } | |||
| 644 | + | return nil, fmt.Errorf("blob store: %w", err) | |||
| 645 | + | } | |||
| 646 | + | log := zerowrap.FromCtx(ctx) | |||
| 647 | + | log.Info().Str("backend", backend).Str("location", location).Msg("blob store ready") | |||
| 648 | + | return blobs, nil | |||
| 649 | + | } | |||
| 650 | + | ||||
| 606 | // pullRequests gives the CI the merge requests, the refs/pull refs and the | 651 | // pullRequests gives the CI the merge requests, the refs/pull refs and the | ||
| 607 | // identity of the test merge commits (CI-3, D5). | 652 | // identity of the test merge commits (CI-3, D5). | ||
| 608 | func (f *forge) pullRequests(cfg *usecase.CIConfig, store *kvstore.Store, git *gitcli.Git, committer domain.Identity) { | 653 | func (f *forge) pullRequests(cfg *usecase.CIConfig, store *kvstore.Store, git *gitcli.Git, committer domain.Identity) { | ||
A internal/app/release_e2e_test.go +160 -0
| @@ -0,0 +1,160 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | package app | |
| 3 | + | ||
| 4 | + | import ( | |
| 5 | + | "context" | |
| 6 | + | "encoding/json" | |
| 7 | + | "fmt" | |
| 8 | + | "io" | |
| 9 | + | "net/http" | |
| 10 | + | "net/http/httptest" | |
| 11 | + | "os" | |
| 12 | + | "path/filepath" | |
| 13 | + | "strings" | |
| 14 | + | "sync/atomic" | |
| 15 | + | "testing" | |
| 16 | + | ||
| 17 | + | "bnema.dev/ermite/internal/adapters/blobdisk" | |
| 18 | + | "bnema.dev/ermite/internal/adapters/ciartifacts" | |
| 19 | + | "bnema.dev/ermite/internal/config" | |
| 20 | + | "bnema.dev/ermite/internal/domain" | |
| 21 | + | "bnema.dev/ermite/internal/ports" | |
| 22 | + | "bnema.dev/ermite/internal/usecase" | |
| 23 | + | "github.com/johannesboyne/gofakes3" | |
| 24 | + | "github.com/johannesboyne/gofakes3/backend/s3mem" | |
| 25 | + | "github.com/stretchr/testify/assert" | |
| 26 | + | "github.com/stretchr/testify/require" | |
| 27 | + | ) | |
| 28 | + | ||
| 29 | + | func TestReleaseBlobForge(t *testing.T) { | |
| 30 | + | for _, backend := range []string{"disk", "s3"} { | |
| 31 | + | t.Run(backend, func(t *testing.T) { | |
| 32 | + | env := map[string]string{} | |
| 33 | + | var admin atomic.Int64 | |
| 34 | + | if backend == "s3" { | |
| 35 | + | mem := s3mem.New() | |
| 36 | + | require.NoError(t, mem.CreateBucket("assets")) | |
| 37 | + | fake := gofakes3.New(mem).Server() | |
| 38 | + | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 39 | + | // Allow only the startup probe/list and object/multipart subset. No bucket administration. | |
| 40 | + | bucket := r.URL.Path == "/assets" || r.URL.Path == "/assets/" | |
| 41 | + | allowed := true | |
| 42 | + | if bucket { | |
| 43 | + | allowed = r.Method == "HEAD" || (r.Method == "GET" && (r.URL.Query().Get("list-type") == "2" || r.URL.Query().Has("uploads"))) | |
| 44 | + | } | |
| 45 | + | for _, key := range []string{"acl", "policy", "tagging", "lifecycle", "versioning", "cors", "notification", "encryption", "website", "logging"} { | |
| 46 | + | if r.URL.Query().Has(key) { | |
| 47 | + | allowed = false | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | if !allowed { | |
| 51 | + | admin.Add(1) | |
| 52 | + | http.Error(w, "unexpected bucket operation", 500) | |
| 53 | + | return | |
| 54 | + | } | |
| 55 | + | fake.ServeHTTP(w, r) | |
| 56 | + | })) | |
| 57 | + | t.Cleanup(srv.Close) | |
| 58 | + | env = map[string]string{config.EnvS3Bucket: "assets", config.EnvS3Endpoint: srv.URL, config.EnvS3Region: "us-east-1", config.EnvS3AccessKeyID: "test-private-id", config.EnvS3SecretAccessKey: "test-private-secret", config.EnvS3ForcePathStyle: "1", config.EnvS3Prefix: "forge/"} | |
| 59 | + | } | |
| 60 | + | h := newHarness(t, harnessOpts{env: env}) | |
| 61 | + | h.publicRepo("demo", "README.md") | |
| 62 | + | rel, err := h.forge.Releases.CreateRelease(h.ctx, domain.Root(), h.name("demo"), ports.CreateReleaseParams{Tag: "v1", TargetCommitish: "main", Title: "Version one"}) | |
| 63 | + | require.NoError(t, err) | |
| 64 | + | asset, err := h.forge.Releases.UploadAsset(h.ctx, domain.Root(), h.name("demo"), ports.UploadAssetParams{ReleaseID: rel.ID, Name: "demo.bin", Size: 7}, strings.NewReader("payload")) | |
| 65 | + | require.NoError(t, err) | |
| 66 | + | if backend == "disk" { | |
| 67 | + | data, err := os.ReadFile(filepath.Join(blobdisk.Dir(h.data), "objects", filepath.FromSlash(asset.BlobKey()))) | |
| 68 | + | require.NoError(t, err) | |
| 69 | + | assert.Equal(t, "payload", string(data)) | |
| 70 | + | } | |
| 71 | + | req, err := http.NewRequestWithContext(h.ctx, "GET", h.url(fmt.Sprintf("/-/github/api/repos/owner/demo/releases/assets/%d", asset.ID)), nil) | |
| 72 | + | require.NoError(t, err) | |
| 73 | + | req.Header.Set("Accept", "application/octet-stream") | |
| 74 | + | req.Header.Set("Range", "bytes=1-3") | |
| 75 | + | res, err := h.srv.Client().Do(req) | |
| 76 | + | require.NoError(t, err) | |
| 77 | + | body, err := io.ReadAll(res.Body) | |
| 78 | + | require.NoError(t, err) | |
| 79 | + | res.Body.Close() | |
| 80 | + | assert.Equal(t, 206, res.StatusCode) | |
| 81 | + | assert.Equal(t, "ayl", string(body)) | |
| 82 | + | assert.Empty(t, res.Header.Get("Location")) | |
| 83 | + | res, err = h.srv.Client().Get(h.url(fmt.Sprintf("/-/github/api/repos/owner/demo/releases/%d", rel.ID))) | |
| 84 | + | require.NoError(t, err) | |
| 85 | + | var obj map[string]any | |
| 86 | + | require.NoError(t, json.NewDecoder(res.Body).Decode(&obj)) | |
| 87 | + | res.Body.Close() | |
| 88 | + | assert.Equal(t, 200, res.StatusCode) | |
| 89 | + | assert.Contains(t, obj["upload_url"], "http://example.test/-/github/api/uploads/") | |
| 90 | + | // New artifacts use the same production backend, not a test-local blob store. | |
| 91 | + | wt := h.workTree("main") | |
| 92 | + | h.mustGit(wt, "fetch", "-q", h.urlAuth(adminToken, "/demo.git"), "main") | |
| 93 | + | h.mustGit(wt, "reset", "--hard", "FETCH_HEAD") | |
| 94 | + | c := &ciHarness{harness: h, root: h.api(adminToken), repo: "demo", wt: wt} | |
| 95 | + | runner := c.registerRunner("alpha", "ubuntu-latest") | |
| 96 | + | _, job, _ := c.newArtifactRun(runner, map[string]string{".github/workflows/ci.yml": artifactWorkflow}) | |
| 97 | + | up := c.jobArtifacts(job) | |
| 98 | + | a := up.upload("dist", []byte("artifact bytes")) | |
| 99 | + | _, data, err := up.download(a.GetId(), 0) | |
| 100 | + | require.NoError(t, err) | |
| 101 | + | assert.Equal(t, "artifact bytes", string(data)) | |
| 102 | + | record, err := h.forge.store.Artifacts().GetArtifact(h.ctx, a.GetId()) | |
| 103 | + | require.NoError(t, err) | |
| 104 | + | assert.Equal(t, domain.CIArtifactStoreBlob, record.Store) | |
| 105 | + | _, err = h.forge.Blobs.Stat(h.ctx, record.BlobKey()) | |
| 106 | + | require.NoError(t, err) | |
| 107 | + | assert.Contains(t, h.logs.String(), "blob store ready") | |
| 108 | + | assert.NotContains(t, h.logs.String(), "test-private-") | |
| 109 | + | assert.Zero(t, admin.Load(), "no bucket-admin operations") | |
| 110 | + | }) | |
| 111 | + | } | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | func TestBlobStartupProbe(t *testing.T) { | |
| 115 | + | _, err := blobStore(context.Background(), config.Config{S3Bucket: "missing", S3Region: "us-east-1"}) | |
| 116 | + | require.Error(t, err) | |
| 117 | + | assert.Contains(t, err.Error(), config.EnvS3Bucket) | |
| 118 | + | mem := s3mem.New() | |
| 119 | + | srv := httptest.NewServer(gofakes3.New(mem).Server()) | |
| 120 | + | defer srv.Close() | |
| 121 | + | _, err = blobStore(context.Background(), config.Config{S3Bucket: "missing", S3Endpoint: srv.URL, S3Region: "us-east-1", S3AccessKeyID: "private-id", S3SecretAccessKey: "private-secret", S3ForcePathStyle: true}) | |
| 122 | + | require.Error(t, err) | |
| 123 | + | assert.Contains(t, err.Error(), "ERMITE_S3_BUCKET") | |
| 124 | + | assert.Contains(t, err.Error(), "not reachable") | |
| 125 | + | assert.NotContains(t, err.Error(), "private-") | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | func TestArtifactLegacyProductionWiring(t *testing.T) { | |
| 129 | + | // Seed the old tree before forge startup, exactly as existing installations do. | |
| 130 | + | data, err := os.MkdirTemp("", "ermite-legacy") | |
| 131 | + | require.NoError(t, err) | |
| 132 | + | t.Cleanup(func() { _ = os.RemoveAll(data) }) | |
| 133 | + | require.NoError(t, prepareDataDir(data)) | |
| 134 | + | require.NoError(t, os.MkdirAll(ciartifacts.Dir(data), 0700)) | |
| 135 | + | h := newHarness(t, harnessOpts{env: map[string]string{config.EnvDataDir: data}}) | |
| 136 | + | c := &ciHarness{harness: h, root: h.api(adminToken), repo: "proj", wt: h.workTree("main")} | |
| 137 | + | _, err = h.forge.Repos.Create(h.ctx, domain.Root(), ports.CreateRepoParams{Name: h.name("proj"), Visibility: domain.VisibilityPrivate}) | |
| 138 | + | require.NoError(t, err) | |
| 139 | + | runner := c.registerRunner("alpha", "ubuntu-latest") | |
| 140 | + | _, job, _ := c.newArtifactRun(runner, map[string]string{".github/workflows/ci.yml": artifactWorkflow}) | |
| 141 | + | up := c.jobArtifacts(job) | |
| 142 | + | created := up.upload("old", []byte("legacy bytes")) | |
| 143 | + | store := h.forge.store.Artifacts() | |
| 144 | + | a, err := store.GetArtifact(h.ctx, created.GetId()) | |
| 145 | + | require.NoError(t, err) | |
| 146 | + | path := filepath.Join(ciartifacts.Dir(data), fmt.Sprintf("%020d", a.RunID), fmt.Sprintf("%020d", a.ID)) | |
| 147 | + | require.NoError(t, os.MkdirAll(filepath.Dir(path), 0700)) | |
| 148 | + | require.NoError(t, os.WriteFile(path, []byte("legacy bytes"), 0600)) | |
| 149 | + | require.NoError(t, h.forge.Blobs.Delete(h.ctx, a.BlobKey())) | |
| 150 | + | a.Store = domain.CIArtifactStoreLegacy | |
| 151 | + | used, version, err := store.ArtifactUsage(h.ctx, a.Repo) | |
| 152 | + | require.NoError(t, err) | |
| 153 | + | require.NoError(t, store.ApplyArtifacts(h.ctx, ports.CIArtifactWrite{Repo: a.Repo, Used: used, UsageVersion: version, Put: []domain.CIArtifact{a}})) | |
| 154 | + | _, body, err := up.download(a.ID, 0) | |
| 155 | + | require.NoError(t, err) | |
| 156 | + | assert.Equal(t, "legacy bytes", string(body)) | |
| 157 | + | // A fresh install does not need or create the legacy tree. | |
| 158 | + | fresh := newHarnessWith(t, harnessOpts{}, func(o *options) { o.ci = func(cfg *usecase.CIConfig) { assert.Nil(t, cfg.ArtifactFiles) } }) | |
| 159 | + | assert.NoDirExists(t, ciartifacts.Dir(fresh.data)) | |
| 160 | + | } | |
| @@ -0,0 +1,160 @@ | |||||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |||
| 2 | + | package app | |||
| 3 | + | ||||
| 4 | + | import ( | |||
| 5 | + | "context" | |||
| 6 | + | "encoding/json" | |||
| 7 | + | "fmt" | |||
| 8 | + | "io" | |||
| 9 | + | "net/http" | |||
| 10 | + | "net/http/httptest" | |||
| 11 | + | "os" | |||
| 12 | + | "path/filepath" | |||
| 13 | + | "strings" | |||
| 14 | + | "sync/atomic" | |||
| 15 | + | "testing" | |||
| 16 | + | ||||
| 17 | + | "bnema.dev/ermite/internal/adapters/blobdisk" | |||
| 18 | + | "bnema.dev/ermite/internal/adapters/ciartifacts" | |||
| 19 | + | "bnema.dev/ermite/internal/config" | |||
| 20 | + | "bnema.dev/ermite/internal/domain" | |||
| 21 | + | "bnema.dev/ermite/internal/ports" | |||
| 22 | + | "bnema.dev/ermite/internal/usecase" | |||
| 23 | + | "github.com/johannesboyne/gofakes3" | |||
| 24 | + | "github.com/johannesboyne/gofakes3/backend/s3mem" | |||
| 25 | + | "github.com/stretchr/testify/assert" | |||
| 26 | + | "github.com/stretchr/testify/require" | |||
| 27 | + | ) | |||
| 28 | + | ||||
| 29 | + | func TestReleaseBlobForge(t *testing.T) { | |||
| 30 | + | for _, backend := range []string{"disk", "s3"} { | |||
| 31 | + | t.Run(backend, func(t *testing.T) { | |||
| 32 | + | env := map[string]string{} | |||
| 33 | + | var admin atomic.Int64 | |||
| 34 | + | if backend == "s3" { | |||
| 35 | + | mem := s3mem.New() | |||
| 36 | + | require.NoError(t, mem.CreateBucket("assets")) | |||
| 37 | + | fake := gofakes3.New(mem).Server() | |||
| 38 | + | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 39 | + | // Allow only the startup probe/list and object/multipart subset. No bucket administration. | |||
| 40 | + | bucket := r.URL.Path == "/assets" || r.URL.Path == "/assets/" | |||
| 41 | + | allowed := true | |||
| 42 | + | if bucket { | |||
| 43 | + | allowed = r.Method == "HEAD" || (r.Method == "GET" && (r.URL.Query().Get("list-type") == "2" || r.URL.Query().Has("uploads"))) | |||
| 44 | + | } | |||
| 45 | + | for _, key := range []string{"acl", "policy", "tagging", "lifecycle", "versioning", "cors", "notification", "encryption", "website", "logging"} { | |||
| 46 | + | if r.URL.Query().Has(key) { | |||
| 47 | + | allowed = false | |||
| 48 | + | } | |||
| 49 | + | } | |||
| 50 | + | if !allowed { | |||
| 51 | + | admin.Add(1) | |||
| 52 | + | http.Error(w, "unexpected bucket operation", 500) | |||
| 53 | + | return | |||
| 54 | + | } | |||
| 55 | + | fake.ServeHTTP(w, r) | |||
| 56 | + | })) | |||
| 57 | + | t.Cleanup(srv.Close) | |||
| 58 | + | env = map[string]string{config.EnvS3Bucket: "assets", config.EnvS3Endpoint: srv.URL, config.EnvS3Region: "us-east-1", config.EnvS3AccessKeyID: "test-private-id", config.EnvS3SecretAccessKey: "test-private-secret", config.EnvS3ForcePathStyle: "1", config.EnvS3Prefix: "forge/"} | |||
| 59 | + | } | |||
| 60 | + | h := newHarness(t, harnessOpts{env: env}) | |||
| 61 | + | h.publicRepo("demo", "README.md") | |||
| 62 | + | rel, err := h.forge.Releases.CreateRelease(h.ctx, domain.Root(), h.name("demo"), ports.CreateReleaseParams{Tag: "v1", TargetCommitish: "main", Title: "Version one"}) | |||
| 63 | + | require.NoError(t, err) | |||
| 64 | + | asset, err := h.forge.Releases.UploadAsset(h.ctx, domain.Root(), h.name("demo"), ports.UploadAssetParams{ReleaseID: rel.ID, Name: "demo.bin", Size: 7}, strings.NewReader("payload")) | |||
| 65 | + | require.NoError(t, err) | |||
| 66 | + | if backend == "disk" { | |||
| 67 | + | data, err := os.ReadFile(filepath.Join(blobdisk.Dir(h.data), "objects", filepath.FromSlash(asset.BlobKey()))) | |||
| 68 | + | require.NoError(t, err) | |||
| 69 | + | assert.Equal(t, "payload", string(data)) | |||
| 70 | + | } | |||
| 71 | + | req, err := http.NewRequestWithContext(h.ctx, "GET", h.url(fmt.Sprintf("/-/github/api/repos/owner/demo/releases/assets/%d", asset.ID)), nil) | |||
| 72 | + | require.NoError(t, err) | |||
| 73 | + | req.Header.Set("Accept", "application/octet-stream") | |||
| 74 | + | req.Header.Set("Range", "bytes=1-3") | |||
| 75 | + | res, err := h.srv.Client().Do(req) | |||
| 76 | + | require.NoError(t, err) | |||
| 77 | + | body, err := io.ReadAll(res.Body) | |||
| 78 | + | require.NoError(t, err) | |||
| 79 | + | res.Body.Close() | |||
| 80 | + | assert.Equal(t, 206, res.StatusCode) | |||
| 81 | + | assert.Equal(t, "ayl", string(body)) | |||
| 82 | + | assert.Empty(t, res.Header.Get("Location")) | |||
| 83 | + | res, err = h.srv.Client().Get(h.url(fmt.Sprintf("/-/github/api/repos/owner/demo/releases/%d", rel.ID))) | |||
| 84 | + | require.NoError(t, err) | |||
| 85 | + | var obj map[string]any | |||
| 86 | + | require.NoError(t, json.NewDecoder(res.Body).Decode(&obj)) | |||
| 87 | + | res.Body.Close() | |||
| 88 | + | assert.Equal(t, 200, res.StatusCode) | |||
| 89 | + | assert.Contains(t, obj["upload_url"], "http://example.test/-/github/api/uploads/") | |||
| 90 | + | // New artifacts use the same production backend, not a test-local blob store. | |||
| 91 | + | wt := h.workTree("main") | |||
| 92 | + | h.mustGit(wt, "fetch", "-q", h.urlAuth(adminToken, "/demo.git"), "main") | |||
| 93 | + | h.mustGit(wt, "reset", "--hard", "FETCH_HEAD") | |||
| 94 | + | c := &ciHarness{harness: h, root: h.api(adminToken), repo: "demo", wt: wt} | |||
| 95 | + | runner := c.registerRunner("alpha", "ubuntu-latest") | |||
| 96 | + | _, job, _ := c.newArtifactRun(runner, map[string]string{".github/workflows/ci.yml": artifactWorkflow}) | |||
| 97 | + | up := c.jobArtifacts(job) | |||
| 98 | + | a := up.upload("dist", []byte("artifact bytes")) | |||
| 99 | + | _, data, err := up.download(a.GetId(), 0) | |||
| 100 | + | require.NoError(t, err) | |||
| 101 | + | assert.Equal(t, "artifact bytes", string(data)) | |||
| 102 | + | record, err := h.forge.store.Artifacts().GetArtifact(h.ctx, a.GetId()) | |||
| 103 | + | require.NoError(t, err) | |||
| 104 | + | assert.Equal(t, domain.CIArtifactStoreBlob, record.Store) | |||
| 105 | + | _, err = h.forge.Blobs.Stat(h.ctx, record.BlobKey()) | |||
| 106 | + | require.NoError(t, err) | |||
| 107 | + | assert.Contains(t, h.logs.String(), "blob store ready") | |||
| 108 | + | assert.NotContains(t, h.logs.String(), "test-private-") | |||
| 109 | + | assert.Zero(t, admin.Load(), "no bucket-admin operations") | |||
| 110 | + | }) | |||
| 111 | + | } | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | func TestBlobStartupProbe(t *testing.T) { | |||
| 115 | + | _, err := blobStore(context.Background(), config.Config{S3Bucket: "missing", S3Region: "us-east-1"}) | |||
| 116 | + | require.Error(t, err) | |||
| 117 | + | assert.Contains(t, err.Error(), config.EnvS3Bucket) | |||
| 118 | + | mem := s3mem.New() | |||
| 119 | + | srv := httptest.NewServer(gofakes3.New(mem).Server()) | |||
| 120 | + | defer srv.Close() | |||
| 121 | + | _, err = blobStore(context.Background(), config.Config{S3Bucket: "missing", S3Endpoint: srv.URL, S3Region: "us-east-1", S3AccessKeyID: "private-id", S3SecretAccessKey: "private-secret", S3ForcePathStyle: true}) | |||
| 122 | + | require.Error(t, err) | |||
| 123 | + | assert.Contains(t, err.Error(), "ERMITE_S3_BUCKET") | |||
| 124 | + | assert.Contains(t, err.Error(), "not reachable") | |||
| 125 | + | assert.NotContains(t, err.Error(), "private-") | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | func TestArtifactLegacyProductionWiring(t *testing.T) { | |||
| 129 | + | // Seed the old tree before forge startup, exactly as existing installations do. | |||
| 130 | + | data, err := os.MkdirTemp("", "ermite-legacy") | |||
| 131 | + | require.NoError(t, err) | |||
| 132 | + | t.Cleanup(func() { _ = os.RemoveAll(data) }) | |||
| 133 | + | require.NoError(t, prepareDataDir(data)) | |||
| 134 | + | require.NoError(t, os.MkdirAll(ciartifacts.Dir(data), 0700)) | |||
| 135 | + | h := newHarness(t, harnessOpts{env: map[string]string{config.EnvDataDir: data}}) | |||
| 136 | + | c := &ciHarness{harness: h, root: h.api(adminToken), repo: "proj", wt: h.workTree("main")} | |||
| 137 | + | _, err = h.forge.Repos.Create(h.ctx, domain.Root(), ports.CreateRepoParams{Name: h.name("proj"), Visibility: domain.VisibilityPrivate}) | |||
| 138 | + | require.NoError(t, err) | |||
| 139 | + | runner := c.registerRunner("alpha", "ubuntu-latest") | |||
| 140 | + | _, job, _ := c.newArtifactRun(runner, map[string]string{".github/workflows/ci.yml": artifactWorkflow}) | |||
| 141 | + | up := c.jobArtifacts(job) | |||
| 142 | + | created := up.upload("old", []byte("legacy bytes")) | |||
| 143 | + | store := h.forge.store.Artifacts() | |||
| 144 | + | a, err := store.GetArtifact(h.ctx, created.GetId()) | |||
| 145 | + | require.NoError(t, err) | |||
| 146 | + | path := filepath.Join(ciartifacts.Dir(data), fmt.Sprintf("%020d", a.RunID), fmt.Sprintf("%020d", a.ID)) | |||
| 147 | + | require.NoError(t, os.MkdirAll(filepath.Dir(path), 0700)) | |||
| 148 | + | require.NoError(t, os.WriteFile(path, []byte("legacy bytes"), 0600)) | |||
| 149 | + | require.NoError(t, h.forge.Blobs.Delete(h.ctx, a.BlobKey())) | |||
| 150 | + | a.Store = domain.CIArtifactStoreLegacy | |||
| 151 | + | used, version, err := store.ArtifactUsage(h.ctx, a.Repo) | |||
| 152 | + | require.NoError(t, err) | |||
| 153 | + | require.NoError(t, store.ApplyArtifacts(h.ctx, ports.CIArtifactWrite{Repo: a.Repo, Used: used, UsageVersion: version, Put: []domain.CIArtifact{a}})) | |||
| 154 | + | _, body, err := up.download(a.ID, 0) | |||
| 155 | + | require.NoError(t, err) | |||
| 156 | + | assert.Equal(t, "legacy bytes", string(body)) | |||
| 157 | + | // A fresh install does not need or create the legacy tree. | |||
| 158 | + | fresh := newHarnessWith(t, harnessOpts{}, func(o *options) { o.ci = func(cfg *usecase.CIConfig) { assert.Nil(t, cfg.ArtifactFiles) } }) | |||
| 159 | + | assert.NoDirExists(t, ciartifacts.Dir(fresh.data)) | |||
| 160 | + | } | |||
M internal/app/testdata/web/blob-binary.html +15 -5
| @@ -93,6 +93,10 @@ | |||
| 93 | 93 | </a> | |
| 94 | 94 | </li> | |
| 95 | 95 | <li> | |
| 96 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 97 | + | </a> | |
| 98 | + | </li> | |
| 99 | + | <li> | |
| 96 | 100 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 97 | 101 | </a> | |
| 98 | 102 | </li> | |
| @@ -358,31 +362,37 @@ | |||
| 358 | 362 | <span class="k">section</span> | |
| 359 | 363 | </a> | |
| 360 | 364 | </li> | |
| 361 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 365 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 366 | + | <a href="/files/releases" tabindex="-1"> | |
| 367 | + | <span class="lbl">releases</span> | |
| 368 | + | <span class="k">section</span> | |
| 369 | + | </a> | |
| 370 | + | </li> | |
| 371 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 362 | 372 | <a href="/files/actions" tabindex="-1"> | |
| 363 | 373 | <span class="lbl">actions</span> | |
| 364 | 374 | <span class="k">section</span> | |
| 365 | 375 | </a> | |
| 366 | 376 | </li> | |
| 367 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 377 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 368 | 378 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 369 | 379 | <span class="lbl">feature/x</span> | |
| 370 | 380 | <span class="k">branch</span> | |
| 371 | 381 | </a> | |
| 372 | 382 | </li> | |
| 373 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 383 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> | |
| 374 | 384 | <a href="/files/tree/main" tabindex="-1"> | |
| 375 | 385 | <span class="lbl">main</span> | |
| 376 | 386 | <span class="k">branch</span> | |
| 377 | 387 | </a> | |
| 378 | 388 | </li> | |
| 379 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 389 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 380 | 390 | <a href="/files/tree/v1.0" tabindex="-1"> | |
| 381 | 391 | <span class="lbl">v1.0</span> | |
| 382 | 392 | <span class="k">tag</span> | |
| 383 | 393 | </a> | |
| 384 | 394 | </li> | |
| 385 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 395 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 386 | 396 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 387 | 397 | <span class="lbl">turn single-key shortcuts off</span> | |
| 388 | 398 | <span class="k">action</span> | |
| @@ -93,6 +93,10 @@ | |||||
| 93 | </a> | 93 | </a> | ||
| 94 | </li> | 94 | </li> | ||
| 95 | <li> | 95 | <li> | ||
| 96 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 97 | + | </a> | |||
| 98 | + | </li> | |||
| 99 | + | <li> | |||
| 96 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 100 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 97 | </a> | 101 | </a> | ||
| 98 | </li> | 102 | </li> | ||
| @@ -358,31 +362,37 @@ | |||||
| 358 | <span class="k">section</span> | 362 | <span class="k">section</span> | ||
| 359 | </a> | 363 | </a> | ||
| 360 | </li> | 364 | </li> | ||
| 361 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 365 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 366 | + | <a href="/files/releases" tabindex="-1"> | |||
| 367 | + | <span class="lbl">releases</span> | |||
| 368 | + | <span class="k">section</span> | |||
| 369 | + | </a> | |||
| 370 | + | </li> | |||
| 371 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 362 | <a href="/files/actions" tabindex="-1"> | 372 | <a href="/files/actions" tabindex="-1"> | ||
| 363 | <span class="lbl">actions</span> | 373 | <span class="lbl">actions</span> | ||
| 364 | <span class="k">section</span> | 374 | <span class="k">section</span> | ||
| 365 | </a> | 375 | </a> | ||
| 366 | </li> | 376 | </li> | ||
| 367 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | 377 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> |
| 368 | <a href="/files/tree/feature/x" tabindex="-1"> | 378 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 369 | <span class="lbl">feature/x</span> | 379 | <span class="lbl">feature/x</span> | ||
| 370 | <span class="k">branch</span> | 380 | <span class="k">branch</span> | ||
| 371 | </a> | 381 | </a> | ||
| 372 | </li> | 382 | </li> | ||
| 373 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | 383 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> |
| 374 | <a href="/files/tree/main" tabindex="-1"> | 384 | <a href="/files/tree/main" tabindex="-1"> | ||
| 375 | <span class="lbl">main</span> | 385 | <span class="lbl">main</span> | ||
| 376 | <span class="k">branch</span> | 386 | <span class="k">branch</span> | ||
| 377 | </a> | 387 | </a> | ||
| 378 | </li> | 388 | </li> | ||
| 379 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | 389 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> |
| 380 | <a href="/files/tree/v1.0" tabindex="-1"> | 390 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 381 | <span class="lbl">v1.0</span> | 391 | <span class="lbl">v1.0</span> | ||
| 382 | <span class="k">tag</span> | 392 | <span class="k">tag</span> | ||
| 383 | </a> | 393 | </a> | ||
| 384 | </li> | 394 | </li> | ||
| 385 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 395 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 386 | <a href="#" data-fire="keys" tabindex="-1"> | 396 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 387 | <span class="lbl">turn single-key shortcuts off</span> | 397 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 388 | <span class="k">action</span> | 398 | <span class="k">action</span> | ||
M internal/app/testdata/web/blob-code.html +15 -5
| @@ -93,6 +93,10 @@ | |||
| 93 | 93 | </a> | |
| 94 | 94 | </li> | |
| 95 | 95 | <li> | |
| 96 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 97 | + | </a> | |
| 98 | + | </li> | |
| 99 | + | <li> | |
| 96 | 100 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 97 | 101 | </a> | |
| 98 | 102 | </li> | |
| @@ -428,31 +432,37 @@ | |||
| 428 | 432 | <span class="k">section</span> | |
| 429 | 433 | </a> | |
| 430 | 434 | </li> | |
| 431 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 435 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 436 | + | <a href="/files/releases" tabindex="-1"> | |
| 437 | + | <span class="lbl">releases</span> | |
| 438 | + | <span class="k">section</span> | |
| 439 | + | </a> | |
| 440 | + | </li> | |
| 441 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 432 | 442 | <a href="/files/actions" tabindex="-1"> | |
| 433 | 443 | <span class="lbl">actions</span> | |
| 434 | 444 | <span class="k">section</span> | |
| 435 | 445 | </a> | |
| 436 | 446 | </li> | |
| 437 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 447 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 438 | 448 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 439 | 449 | <span class="lbl">feature/x</span> | |
| 440 | 450 | <span class="k">branch</span> | |
| 441 | 451 | </a> | |
| 442 | 452 | </li> | |
| 443 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 453 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> | |
| 444 | 454 | <a href="/files/tree/main" tabindex="-1"> | |
| 445 | 455 | <span class="lbl">main</span> | |
| 446 | 456 | <span class="k">branch</span> | |
| 447 | 457 | </a> | |
| 448 | 458 | </li> | |
| 449 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 459 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 450 | 460 | <a href="/files/tree/v1.0" tabindex="-1"> | |
| 451 | 461 | <span class="lbl">v1.0</span> | |
| 452 | 462 | <span class="k">tag</span> | |
| 453 | 463 | </a> | |
| 454 | 464 | </li> | |
| 455 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 465 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 456 | 466 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 457 | 467 | <span class="lbl">turn single-key shortcuts off</span> | |
| 458 | 468 | <span class="k">action</span> | |
| @@ -93,6 +93,10 @@ | |||||
| 93 | </a> | 93 | </a> | ||
| 94 | </li> | 94 | </li> | ||
| 95 | <li> | 95 | <li> | ||
| 96 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 97 | + | </a> | |||
| 98 | + | </li> | |||
| 99 | + | <li> | |||
| 96 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 100 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 97 | </a> | 101 | </a> | ||
| 98 | </li> | 102 | </li> | ||
| @@ -428,31 +432,37 @@ | |||||
| 428 | <span class="k">section</span> | 432 | <span class="k">section</span> | ||
| 429 | </a> | 433 | </a> | ||
| 430 | </li> | 434 | </li> | ||
| 431 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 435 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 436 | + | <a href="/files/releases" tabindex="-1"> | |||
| 437 | + | <span class="lbl">releases</span> | |||
| 438 | + | <span class="k">section</span> | |||
| 439 | + | </a> | |||
| 440 | + | </li> | |||
| 441 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 432 | <a href="/files/actions" tabindex="-1"> | 442 | <a href="/files/actions" tabindex="-1"> | ||
| 433 | <span class="lbl">actions</span> | 443 | <span class="lbl">actions</span> | ||
| 434 | <span class="k">section</span> | 444 | <span class="k">section</span> | ||
| 435 | </a> | 445 | </a> | ||
| 436 | </li> | 446 | </li> | ||
| 437 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | 447 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> |
| 438 | <a href="/files/tree/feature/x" tabindex="-1"> | 448 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 439 | <span class="lbl">feature/x</span> | 449 | <span class="lbl">feature/x</span> | ||
| 440 | <span class="k">branch</span> | 450 | <span class="k">branch</span> | ||
| 441 | </a> | 451 | </a> | ||
| 442 | </li> | 452 | </li> | ||
| 443 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | 453 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> |
| 444 | <a href="/files/tree/main" tabindex="-1"> | 454 | <a href="/files/tree/main" tabindex="-1"> | ||
| 445 | <span class="lbl">main</span> | 455 | <span class="lbl">main</span> | ||
| 446 | <span class="k">branch</span> | 456 | <span class="k">branch</span> | ||
| 447 | </a> | 457 | </a> | ||
| 448 | </li> | 458 | </li> | ||
| 449 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | 459 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> |
| 450 | <a href="/files/tree/v1.0" tabindex="-1"> | 460 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 451 | <span class="lbl">v1.0</span> | 461 | <span class="lbl">v1.0</span> | ||
| 452 | <span class="k">tag</span> | 462 | <span class="k">tag</span> | ||
| 453 | </a> | 463 | </a> | ||
| 454 | </li> | 464 | </li> | ||
| 455 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 465 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 456 | <a href="#" data-fire="keys" tabindex="-1"> | 466 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 457 | <span class="lbl">turn single-key shortcuts off</span> | 467 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 458 | <span class="k">action</span> | 468 | <span class="k">action</span> | ||
M internal/app/testdata/web/blob-empty.html +15 -5
| @@ -93,6 +93,10 @@ | |||
| 93 | 93 | </a> | |
| 94 | 94 | </li> | |
| 95 | 95 | <li> | |
| 96 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 97 | + | </a> | |
| 98 | + | </li> | |
| 99 | + | <li> | |
| 96 | 100 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 97 | 101 | </a> | |
| 98 | 102 | </li> | |
| @@ -358,31 +362,37 @@ | |||
| 358 | 362 | <span class="k">section</span> | |
| 359 | 363 | </a> | |
| 360 | 364 | </li> | |
| 361 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 365 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 366 | + | <a href="/files/releases" tabindex="-1"> | |
| 367 | + | <span class="lbl">releases</span> | |
| 368 | + | <span class="k">section</span> | |
| 369 | + | </a> | |
| 370 | + | </li> | |
| 371 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 362 | 372 | <a href="/files/actions" tabindex="-1"> | |
| 363 | 373 | <span class="lbl">actions</span> | |
| 364 | 374 | <span class="k">section</span> | |
| 365 | 375 | </a> | |
| 366 | 376 | </li> | |
| 367 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 377 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 368 | 378 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 369 | 379 | <span class="lbl">feature/x</span> | |
| 370 | 380 | <span class="k">branch</span> | |
| 371 | 381 | </a> | |
| 372 | 382 | </li> | |
| 373 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 383 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> | |
| 374 | 384 | <a href="/files/tree/main" tabindex="-1"> | |
| 375 | 385 | <span class="lbl">main</span> | |
| 376 | 386 | <span class="k">branch</span> | |
| 377 | 387 | </a> | |
| 378 | 388 | </li> | |
| 379 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 389 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 380 | 390 | <a href="/files/tree/v1.0" tabindex="-1"> | |
| 381 | 391 | <span class="lbl">v1.0</span> | |
| 382 | 392 | <span class="k">tag</span> | |
| 383 | 393 | </a> | |
| 384 | 394 | </li> | |
| 385 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 395 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 386 | 396 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 387 | 397 | <span class="lbl">turn single-key shortcuts off</span> | |
| 388 | 398 | <span class="k">action</span> | |
| @@ -93,6 +93,10 @@ | |||||
| 93 | </a> | 93 | </a> | ||
| 94 | </li> | 94 | </li> | ||
| 95 | <li> | 95 | <li> | ||
| 96 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 97 | + | </a> | |||
| 98 | + | </li> | |||
| 99 | + | <li> | |||
| 96 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 100 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 97 | </a> | 101 | </a> | ||
| 98 | </li> | 102 | </li> | ||
| @@ -358,31 +362,37 @@ | |||||
| 358 | <span class="k">section</span> | 362 | <span class="k">section</span> | ||
| 359 | </a> | 363 | </a> | ||
| 360 | </li> | 364 | </li> | ||
| 361 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 365 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 366 | + | <a href="/files/releases" tabindex="-1"> | |||
| 367 | + | <span class="lbl">releases</span> | |||
| 368 | + | <span class="k">section</span> | |||
| 369 | + | </a> | |||
| 370 | + | </li> | |||
| 371 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 362 | <a href="/files/actions" tabindex="-1"> | 372 | <a href="/files/actions" tabindex="-1"> | ||
| 363 | <span class="lbl">actions</span> | 373 | <span class="lbl">actions</span> | ||
| 364 | <span class="k">section</span> | 374 | <span class="k">section</span> | ||
| 365 | </a> | 375 | </a> | ||
| 366 | </li> | 376 | </li> | ||
| 367 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | 377 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> |
| 368 | <a href="/files/tree/feature/x" tabindex="-1"> | 378 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 369 | <span class="lbl">feature/x</span> | 379 | <span class="lbl">feature/x</span> | ||
| 370 | <span class="k">branch</span> | 380 | <span class="k">branch</span> | ||
| 371 | </a> | 381 | </a> | ||
| 372 | </li> | 382 | </li> | ||
| 373 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | 383 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> |
| 374 | <a href="/files/tree/main" tabindex="-1"> | 384 | <a href="/files/tree/main" tabindex="-1"> | ||
| 375 | <span class="lbl">main</span> | 385 | <span class="lbl">main</span> | ||
| 376 | <span class="k">branch</span> | 386 | <span class="k">branch</span> | ||
| 377 | </a> | 387 | </a> | ||
| 378 | </li> | 388 | </li> | ||
| 379 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | 389 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> |
| 380 | <a href="/files/tree/v1.0" tabindex="-1"> | 390 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 381 | <span class="lbl">v1.0</span> | 391 | <span class="lbl">v1.0</span> | ||
| 382 | <span class="k">tag</span> | 392 | <span class="k">tag</span> | ||
| 383 | </a> | 393 | </a> | ||
| 384 | </li> | 394 | </li> | ||
| 385 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 395 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 386 | <a href="#" data-fire="keys" tabindex="-1"> | 396 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 387 | <span class="lbl">turn single-key shortcuts off</span> | 397 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 388 | <span class="k">action</span> | 398 | <span class="k">action</span> | ||
M internal/app/testdata/web/blob-image.html +15 -5
| @@ -93,6 +93,10 @@ | |||
| 93 | 93 | </a> | |
| 94 | 94 | </li> | |
| 95 | 95 | <li> | |
| 96 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 97 | + | </a> | |
| 98 | + | </li> | |
| 99 | + | <li> | |
| 96 | 100 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 97 | 101 | </a> | |
| 98 | 102 | </li> | |
| @@ -360,31 +364,37 @@ | |||
| 360 | 364 | <span class="k">section</span> | |
| 361 | 365 | </a> | |
| 362 | 366 | </li> | |
| 363 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 367 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 368 | + | <a href="/files/releases" tabindex="-1"> | |
| 369 | + | <span class="lbl">releases</span> | |
| 370 | + | <span class="k">section</span> | |
| 371 | + | </a> | |
| 372 | + | </li> | |
| 373 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 364 | 374 | <a href="/files/actions" tabindex="-1"> | |
| 365 | 375 | <span class="lbl">actions</span> | |
| 366 | 376 | <span class="k">section</span> | |
| 367 | 377 | </a> | |
| 368 | 378 | </li> | |
| 369 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 379 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 370 | 380 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 371 | 381 | <span class="lbl">feature/x</span> | |
| 372 | 382 | <span class="k">branch</span> | |
| 373 | 383 | </a> | |
| 374 | 384 | </li> | |
| 375 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 385 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> | |
| 376 | 386 | <a href="/files/tree/main" tabindex="-1"> | |
| 377 | 387 | <span class="lbl">main</span> | |
| 378 | 388 | <span class="k">branch</span> | |
| 379 | 389 | </a> | |
| 380 | 390 | </li> | |
| 381 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 391 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 382 | 392 | <a href="/files/tree/v1.0" tabindex="-1"> | |
| 383 | 393 | <span class="lbl">v1.0</span> | |
| 384 | 394 | <span class="k">tag</span> | |
| 385 | 395 | </a> | |
| 386 | 396 | </li> | |
| 387 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 397 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 388 | 398 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 389 | 399 | <span class="lbl">turn single-key shortcuts off</span> | |
| 390 | 400 | <span class="k">action</span> | |
| @@ -93,6 +93,10 @@ | |||||
| 93 | </a> | 93 | </a> | ||
| 94 | </li> | 94 | </li> | ||
| 95 | <li> | 95 | <li> | ||
| 96 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 97 | + | </a> | |||
| 98 | + | </li> | |||
| 99 | + | <li> | |||
| 96 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 100 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 97 | </a> | 101 | </a> | ||
| 98 | </li> | 102 | </li> | ||
| @@ -360,31 +364,37 @@ | |||||
| 360 | <span class="k">section</span> | 364 | <span class="k">section</span> | ||
| 361 | </a> | 365 | </a> | ||
| 362 | </li> | 366 | </li> | ||
| 363 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 367 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 368 | + | <a href="/files/releases" tabindex="-1"> | |||
| 369 | + | <span class="lbl">releases</span> | |||
| 370 | + | <span class="k">section</span> | |||
| 371 | + | </a> | |||
| 372 | + | </li> | |||
| 373 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 364 | <a href="/files/actions" tabindex="-1"> | 374 | <a href="/files/actions" tabindex="-1"> | ||
| 365 | <span class="lbl">actions</span> | 375 | <span class="lbl">actions</span> | ||
| 366 | <span class="k">section</span> | 376 | <span class="k">section</span> | ||
| 367 | </a> | 377 | </a> | ||
| 368 | </li> | 378 | </li> | ||
| 369 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | 379 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> |
| 370 | <a href="/files/tree/feature/x" tabindex="-1"> | 380 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 371 | <span class="lbl">feature/x</span> | 381 | <span class="lbl">feature/x</span> | ||
| 372 | <span class="k">branch</span> | 382 | <span class="k">branch</span> | ||
| 373 | </a> | 383 | </a> | ||
| 374 | </li> | 384 | </li> | ||
| 375 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | 385 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> |
| 376 | <a href="/files/tree/main" tabindex="-1"> | 386 | <a href="/files/tree/main" tabindex="-1"> | ||
| 377 | <span class="lbl">main</span> | 387 | <span class="lbl">main</span> | ||
| 378 | <span class="k">branch</span> | 388 | <span class="k">branch</span> | ||
| 379 | </a> | 389 | </a> | ||
| 380 | </li> | 390 | </li> | ||
| 381 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | 391 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> |
| 382 | <a href="/files/tree/v1.0" tabindex="-1"> | 392 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 383 | <span class="lbl">v1.0</span> | 393 | <span class="lbl">v1.0</span> | ||
| 384 | <span class="k">tag</span> | 394 | <span class="k">tag</span> | ||
| 385 | </a> | 395 | </a> | ||
| 386 | </li> | 396 | </li> | ||
| 387 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 397 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 388 | <a href="#" data-fire="keys" tabindex="-1"> | 398 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 389 | <span class="lbl">turn single-key shortcuts off</span> | 399 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 390 | <span class="k">action</span> | 400 | <span class="k">action</span> | ||
M internal/app/testdata/web/blob-markdown.html +15 -5
| @@ -96,6 +96,10 @@ | |||
| 96 | 96 | </a> | |
| 97 | 97 | </li> | |
| 98 | 98 | <li> | |
| 99 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 100 | + | </a> | |
| 101 | + | </li> | |
| 102 | + | <li> | |
| 99 | 103 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 100 | 104 | </a> | |
| 101 | 105 | </li> | |
| @@ -425,31 +429,37 @@ | |||
| 425 | 429 | <span class="k">section</span> | |
| 426 | 430 | </a> | |
| 427 | 431 | </li> | |
| 428 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 432 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 433 | + | <a href="/files/releases" tabindex="-1"> | |
| 434 | + | <span class="lbl">releases</span> | |
| 435 | + | <span class="k">section</span> | |
| 436 | + | </a> | |
| 437 | + | </li> | |
| 438 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 429 | 439 | <a href="/files/actions" tabindex="-1"> | |
| 430 | 440 | <span class="lbl">actions</span> | |
| 431 | 441 | <span class="k">section</span> | |
| 432 | 442 | </a> | |
| 433 | 443 | </li> | |
| 434 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 444 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 435 | 445 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 436 | 446 | <span class="lbl">feature/x</span> | |
| 437 | 447 | <span class="k">branch</span> | |
| 438 | 448 | </a> | |
| 439 | 449 | </li> | |
| 440 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 450 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> | |
| 441 | 451 | <a href="/files/tree/main" tabindex="-1"> | |
| 442 | 452 | <span class="lbl">main</span> | |
| 443 | 453 | <span class="k">branch</span> | |
| 444 | 454 | </a> | |
| 445 | 455 | </li> | |
| 446 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 456 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 447 | 457 | <a href="/files/tree/v1.0" tabindex="-1"> | |
| 448 | 458 | <span class="lbl">v1.0</span> | |
| 449 | 459 | <span class="k">tag</span> | |
| 450 | 460 | </a> | |
| 451 | 461 | </li> | |
| 452 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 462 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 453 | 463 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 454 | 464 | <span class="lbl">turn single-key shortcuts off</span> | |
| 455 | 465 | <span class="k">action</span> | |
| @@ -96,6 +96,10 @@ | |||||
| 96 | </a> | 96 | </a> | ||
| 97 | </li> | 97 | </li> | ||
| 98 | <li> | 98 | <li> | ||
| 99 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 100 | + | </a> | |||
| 101 | + | </li> | |||
| 102 | + | <li> | |||
| 99 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 103 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 100 | </a> | 104 | </a> | ||
| 101 | </li> | 105 | </li> | ||
| @@ -425,31 +429,37 @@ | |||||
| 425 | <span class="k">section</span> | 429 | <span class="k">section</span> | ||
| 426 | </a> | 430 | </a> | ||
| 427 | </li> | 431 | </li> | ||
| 428 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 432 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 433 | + | <a href="/files/releases" tabindex="-1"> | |||
| 434 | + | <span class="lbl">releases</span> | |||
| 435 | + | <span class="k">section</span> | |||
| 436 | + | </a> | |||
| 437 | + | </li> | |||
| 438 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 429 | <a href="/files/actions" tabindex="-1"> | 439 | <a href="/files/actions" tabindex="-1"> | ||
| 430 | <span class="lbl">actions</span> | 440 | <span class="lbl">actions</span> | ||
| 431 | <span class="k">section</span> | 441 | <span class="k">section</span> | ||
| 432 | </a> | 442 | </a> | ||
| 433 | </li> | 443 | </li> | ||
| 434 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | 444 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> |
| 435 | <a href="/files/tree/feature/x" tabindex="-1"> | 445 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 436 | <span class="lbl">feature/x</span> | 446 | <span class="lbl">feature/x</span> | ||
| 437 | <span class="k">branch</span> | 447 | <span class="k">branch</span> | ||
| 438 | </a> | 448 | </a> | ||
| 439 | </li> | 449 | </li> | ||
| 440 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | 450 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> |
| 441 | <a href="/files/tree/main" tabindex="-1"> | 451 | <a href="/files/tree/main" tabindex="-1"> | ||
| 442 | <span class="lbl">main</span> | 452 | <span class="lbl">main</span> | ||
| 443 | <span class="k">branch</span> | 453 | <span class="k">branch</span> | ||
| 444 | </a> | 454 | </a> | ||
| 445 | </li> | 455 | </li> | ||
| 446 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | 456 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> |
| 447 | <a href="/files/tree/v1.0" tabindex="-1"> | 457 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 448 | <span class="lbl">v1.0</span> | 458 | <span class="lbl">v1.0</span> | ||
| 449 | <span class="k">tag</span> | 459 | <span class="k">tag</span> | ||
| 450 | </a> | 460 | </a> | ||
| 451 | </li> | 461 | </li> | ||
| 452 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 462 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 453 | <a href="#" data-fire="keys" tabindex="-1"> | 463 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 454 | <span class="lbl">turn single-key shortcuts off</span> | 464 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 455 | <span class="k">action</span> | 465 | <span class="k">action</span> | ||
M internal/app/testdata/web/blob-svg.html +15 -5
| @@ -93,6 +93,10 @@ | |||
| 93 | 93 | </a> | |
| 94 | 94 | </li> | |
| 95 | 95 | <li> | |
| 96 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 97 | + | </a> | |
| 98 | + | </li> | |
| 99 | + | <li> | |
| 96 | 100 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 97 | 101 | </a> | |
| 98 | 102 | </li> | |
| @@ -383,31 +387,37 @@ | |||
| 383 | 387 | <span class="k">section</span> | |
| 384 | 388 | </a> | |
| 385 | 389 | </li> | |
| 386 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 390 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 391 | + | <a href="/files/releases" tabindex="-1"> | |
| 392 | + | <span class="lbl">releases</span> | |
| 393 | + | <span class="k">section</span> | |
| 394 | + | </a> | |
| 395 | + | </li> | |
| 396 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 387 | 397 | <a href="/files/actions" tabindex="-1"> | |
| 388 | 398 | <span class="lbl">actions</span> | |
| 389 | 399 | <span class="k">section</span> | |
| 390 | 400 | </a> | |
| 391 | 401 | </li> | |
| 392 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 402 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 393 | 403 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 394 | 404 | <span class="lbl">feature/x</span> | |
| 395 | 405 | <span class="k">branch</span> | |
| 396 | 406 | </a> | |
| 397 | 407 | </li> | |
| 398 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 408 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> | |
| 399 | 409 | <a href="/files/tree/main" tabindex="-1"> | |
| 400 | 410 | <span class="lbl">main</span> | |
| 401 | 411 | <span class="k">branch</span> | |
| 402 | 412 | </a> | |
| 403 | 413 | </li> | |
| 404 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 414 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 405 | 415 | <a href="/files/tree/v1.0" tabindex="-1"> | |
| 406 | 416 | <span class="lbl">v1.0</span> | |
| 407 | 417 | <span class="k">tag</span> | |
| 408 | 418 | </a> | |
| 409 | 419 | </li> | |
| 410 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 420 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 411 | 421 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 412 | 422 | <span class="lbl">turn single-key shortcuts off</span> | |
| 413 | 423 | <span class="k">action</span> | |
| @@ -93,6 +93,10 @@ | |||||
| 93 | </a> | 93 | </a> | ||
| 94 | </li> | 94 | </li> | ||
| 95 | <li> | 95 | <li> | ||
| 96 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 97 | + | </a> | |||
| 98 | + | </li> | |||
| 99 | + | <li> | |||
| 96 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 100 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 97 | </a> | 101 | </a> | ||
| 98 | </li> | 102 | </li> | ||
| @@ -383,31 +387,37 @@ | |||||
| 383 | <span class="k">section</span> | 387 | <span class="k">section</span> | ||
| 384 | </a> | 388 | </a> | ||
| 385 | </li> | 389 | </li> | ||
| 386 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 390 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 391 | + | <a href="/files/releases" tabindex="-1"> | |||
| 392 | + | <span class="lbl">releases</span> | |||
| 393 | + | <span class="k">section</span> | |||
| 394 | + | </a> | |||
| 395 | + | </li> | |||
| 396 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 387 | <a href="/files/actions" tabindex="-1"> | 397 | <a href="/files/actions" tabindex="-1"> | ||
| 388 | <span class="lbl">actions</span> | 398 | <span class="lbl">actions</span> | ||
| 389 | <span class="k">section</span> | 399 | <span class="k">section</span> | ||
| 390 | </a> | 400 | </a> | ||
| 391 | </li> | 401 | </li> | ||
| 392 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | 402 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> |
| 393 | <a href="/files/tree/feature/x" tabindex="-1"> | 403 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 394 | <span class="lbl">feature/x</span> | 404 | <span class="lbl">feature/x</span> | ||
| 395 | <span class="k">branch</span> | 405 | <span class="k">branch</span> | ||
| 396 | </a> | 406 | </a> | ||
| 397 | </li> | 407 | </li> | ||
| 398 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | 408 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> |
| 399 | <a href="/files/tree/main" tabindex="-1"> | 409 | <a href="/files/tree/main" tabindex="-1"> | ||
| 400 | <span class="lbl">main</span> | 410 | <span class="lbl">main</span> | ||
| 401 | <span class="k">branch</span> | 411 | <span class="k">branch</span> | ||
| 402 | </a> | 412 | </a> | ||
| 403 | </li> | 413 | </li> | ||
| 404 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | 414 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> |
| 405 | <a href="/files/tree/v1.0" tabindex="-1"> | 415 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 406 | <span class="lbl">v1.0</span> | 416 | <span class="lbl">v1.0</span> | ||
| 407 | <span class="k">tag</span> | 417 | <span class="k">tag</span> | ||
| 408 | </a> | 418 | </a> | ||
| 409 | </li> | 419 | </li> | ||
| 410 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 420 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 411 | <a href="#" data-fire="keys" tabindex="-1"> | 421 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 412 | <span class="lbl">turn single-key shortcuts off</span> | 422 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 413 | <span class="k">action</span> | 423 | <span class="k">action</span> | ||
M internal/app/testdata/web/blob-text.html +15 -5
| @@ -93,6 +93,10 @@ | |||
| 93 | 93 | </a> | |
| 94 | 94 | </li> | |
| 95 | 95 | <li> | |
| 96 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 97 | + | </a> | |
| 98 | + | </li> | |
| 99 | + | <li> | |
| 96 | 100 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 97 | 101 | </a> | |
| 98 | 102 | </li> | |
| @@ -378,31 +382,37 @@ | |||
| 378 | 382 | <span class="k">section</span> | |
| 379 | 383 | </a> | |
| 380 | 384 | </li> | |
| 381 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 385 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 386 | + | <a href="/files/releases" tabindex="-1"> | |
| 387 | + | <span class="lbl">releases</span> | |
| 388 | + | <span class="k">section</span> | |
| 389 | + | </a> | |
| 390 | + | </li> | |
| 391 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 382 | 392 | <a href="/files/actions" tabindex="-1"> | |
| 383 | 393 | <span class="lbl">actions</span> | |
| 384 | 394 | <span class="k">section</span> | |
| 385 | 395 | </a> | |
| 386 | 396 | </li> | |
| 387 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 397 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 388 | 398 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 389 | 399 | <span class="lbl">feature/x</span> | |
| 390 | 400 | <span class="k">branch</span> | |
| 391 | 401 | </a> | |
| 392 | 402 | </li> | |
| 393 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 403 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> | |
| 394 | 404 | <a href="/files/tree/main" tabindex="-1"> | |
| 395 | 405 | <span class="lbl">main</span> | |
| 396 | 406 | <span class="k">branch</span> | |
| 397 | 407 | </a> | |
| 398 | 408 | </li> | |
| 399 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 409 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 400 | 410 | <a href="/files/tree/v1.0" tabindex="-1"> | |
| 401 | 411 | <span class="lbl">v1.0</span> | |
| 402 | 412 | <span class="k">tag</span> | |
| 403 | 413 | </a> | |
| 404 | 414 | </li> | |
| 405 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 415 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 406 | 416 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 407 | 417 | <span class="lbl">turn single-key shortcuts off</span> | |
| 408 | 418 | <span class="k">action</span> | |
| @@ -93,6 +93,10 @@ | |||||
| 93 | </a> | 93 | </a> | ||
| 94 | </li> | 94 | </li> | ||
| 95 | <li> | 95 | <li> | ||
| 96 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 97 | + | </a> | |||
| 98 | + | </li> | |||
| 99 | + | <li> | |||
| 96 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 100 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 97 | </a> | 101 | </a> | ||
| 98 | </li> | 102 | </li> | ||
| @@ -378,31 +382,37 @@ | |||||
| 378 | <span class="k">section</span> | 382 | <span class="k">section</span> | ||
| 379 | </a> | 383 | </a> | ||
| 380 | </li> | 384 | </li> | ||
| 381 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 385 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 386 | + | <a href="/files/releases" tabindex="-1"> | |||
| 387 | + | <span class="lbl">releases</span> | |||
| 388 | + | <span class="k">section</span> | |||
| 389 | + | </a> | |||
| 390 | + | </li> | |||
| 391 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 382 | <a href="/files/actions" tabindex="-1"> | 392 | <a href="/files/actions" tabindex="-1"> | ||
| 383 | <span class="lbl">actions</span> | 393 | <span class="lbl">actions</span> | ||
| 384 | <span class="k">section</span> | 394 | <span class="k">section</span> | ||
| 385 | </a> | 395 | </a> | ||
| 386 | </li> | 396 | </li> | ||
| 387 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | 397 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> |
| 388 | <a href="/files/tree/feature/x" tabindex="-1"> | 398 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 389 | <span class="lbl">feature/x</span> | 399 | <span class="lbl">feature/x</span> | ||
| 390 | <span class="k">branch</span> | 400 | <span class="k">branch</span> | ||
| 391 | </a> | 401 | </a> | ||
| 392 | </li> | 402 | </li> | ||
| 393 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | 403 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> |
| 394 | <a href="/files/tree/main" tabindex="-1"> | 404 | <a href="/files/tree/main" tabindex="-1"> | ||
| 395 | <span class="lbl">main</span> | 405 | <span class="lbl">main</span> | ||
| 396 | <span class="k">branch</span> | 406 | <span class="k">branch</span> | ||
| 397 | </a> | 407 | </a> | ||
| 398 | </li> | 408 | </li> | ||
| 399 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | 409 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> |
| 400 | <a href="/files/tree/v1.0" tabindex="-1"> | 410 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 401 | <span class="lbl">v1.0</span> | 411 | <span class="lbl">v1.0</span> | ||
| 402 | <span class="k">tag</span> | 412 | <span class="k">tag</span> | ||
| 403 | </a> | 413 | </a> | ||
| 404 | </li> | 414 | </li> | ||
| 405 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 415 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 406 | <a href="#" data-fire="keys" tabindex="-1"> | 416 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 407 | <span class="lbl">turn single-key shortcuts off</span> | 417 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 408 | <span class="k">action</span> | 418 | <span class="k">action</span> | ||
M internal/app/testdata/web/blob-too-large.html +15 -5
| @@ -93,6 +93,10 @@ | |||
| 93 | 93 | </a> | |
| 94 | 94 | </li> | |
| 95 | 95 | <li> | |
| 96 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 97 | + | </a> | |
| 98 | + | </li> | |
| 99 | + | <li> | |
| 96 | 100 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 97 | 101 | </a> | |
| 98 | 102 | </li> | |
| @@ -358,31 +362,37 @@ | |||
| 358 | 362 | <span class="k">section</span> | |
| 359 | 363 | </a> | |
| 360 | 364 | </li> | |
| 361 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 365 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 366 | + | <a href="/files/releases" tabindex="-1"> | |
| 367 | + | <span class="lbl">releases</span> | |
| 368 | + | <span class="k">section</span> | |
| 369 | + | </a> | |
| 370 | + | </li> | |
| 371 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 362 | 372 | <a href="/files/actions" tabindex="-1"> | |
| 363 | 373 | <span class="lbl">actions</span> | |
| 364 | 374 | <span class="k">section</span> | |
| 365 | 375 | </a> | |
| 366 | 376 | </li> | |
| 367 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 377 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 368 | 378 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 369 | 379 | <span class="lbl">feature/x</span> | |
| 370 | 380 | <span class="k">branch</span> | |
| 371 | 381 | </a> | |
| 372 | 382 | </li> | |
| 373 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 383 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> | |
| 374 | 384 | <a href="/files/tree/main" tabindex="-1"> | |
| 375 | 385 | <span class="lbl">main</span> | |
| 376 | 386 | <span class="k">branch</span> | |
| 377 | 387 | </a> | |
| 378 | 388 | </li> | |
| 379 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 389 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 380 | 390 | <a href="/files/tree/v1.0" tabindex="-1"> | |
| 381 | 391 | <span class="lbl">v1.0</span> | |
| 382 | 392 | <span class="k">tag</span> | |
| 383 | 393 | </a> | |
| 384 | 394 | </li> | |
| 385 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 395 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 386 | 396 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 387 | 397 | <span class="lbl">turn single-key shortcuts off</span> | |
| 388 | 398 | <span class="k">action</span> | |
| @@ -93,6 +93,10 @@ | |||||
| 93 | </a> | 93 | </a> | ||
| 94 | </li> | 94 | </li> | ||
| 95 | <li> | 95 | <li> | ||
| 96 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 97 | + | </a> | |||
| 98 | + | </li> | |||
| 99 | + | <li> | |||
| 96 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 100 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 97 | </a> | 101 | </a> | ||
| 98 | </li> | 102 | </li> | ||
| @@ -358,31 +362,37 @@ | |||||
| 358 | <span class="k">section</span> | 362 | <span class="k">section</span> | ||
| 359 | </a> | 363 | </a> | ||
| 360 | </li> | 364 | </li> | ||
| 361 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 365 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 366 | + | <a href="/files/releases" tabindex="-1"> | |||
| 367 | + | <span class="lbl">releases</span> | |||
| 368 | + | <span class="k">section</span> | |||
| 369 | + | </a> | |||
| 370 | + | </li> | |||
| 371 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 362 | <a href="/files/actions" tabindex="-1"> | 372 | <a href="/files/actions" tabindex="-1"> | ||
| 363 | <span class="lbl">actions</span> | 373 | <span class="lbl">actions</span> | ||
| 364 | <span class="k">section</span> | 374 | <span class="k">section</span> | ||
| 365 | </a> | 375 | </a> | ||
| 366 | </li> | 376 | </li> | ||
| 367 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | 377 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> |
| 368 | <a href="/files/tree/feature/x" tabindex="-1"> | 378 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 369 | <span class="lbl">feature/x</span> | 379 | <span class="lbl">feature/x</span> | ||
| 370 | <span class="k">branch</span> | 380 | <span class="k">branch</span> | ||
| 371 | </a> | 381 | </a> | ||
| 372 | </li> | 382 | </li> | ||
| 373 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | 383 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> |
| 374 | <a href="/files/tree/main" tabindex="-1"> | 384 | <a href="/files/tree/main" tabindex="-1"> | ||
| 375 | <span class="lbl">main</span> | 385 | <span class="lbl">main</span> | ||
| 376 | <span class="k">branch</span> | 386 | <span class="k">branch</span> | ||
| 377 | </a> | 387 | </a> | ||
| 378 | </li> | 388 | </li> | ||
| 379 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | 389 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> |
| 380 | <a href="/files/tree/v1.0" tabindex="-1"> | 390 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 381 | <span class="lbl">v1.0</span> | 391 | <span class="lbl">v1.0</span> | ||
| 382 | <span class="k">tag</span> | 392 | <span class="k">tag</span> | ||
| 383 | </a> | 393 | </a> | ||
| 384 | </li> | 394 | </li> | ||
| 385 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 395 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 386 | <a href="#" data-fire="keys" tabindex="-1"> | 396 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 387 | <span class="lbl">turn single-key shortcuts off</span> | 397 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 388 | <span class="k">action</span> | 398 | <span class="k">action</span> | ||
M internal/app/testdata/web/blob-trojan.html +15 -5
| @@ -93,6 +93,10 @@ | |||
| 93 | 93 | </a> | |
| 94 | 94 | </li> | |
| 95 | 95 | <li> | |
| 96 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 97 | + | </a> | |
| 98 | + | </li> | |
| 99 | + | <li> | |
| 96 | 100 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 97 | 101 | </a> | |
| 98 | 102 | </li> | |
| @@ -425,31 +429,37 @@ | |||
| 425 | 429 | <span class="k">section</span> | |
| 426 | 430 | </a> | |
| 427 | 431 | </li> | |
| 428 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 432 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 433 | + | <a href="/files/releases" tabindex="-1"> | |
| 434 | + | <span class="lbl">releases</span> | |
| 435 | + | <span class="k">section</span> | |
| 436 | + | </a> | |
| 437 | + | </li> | |
| 438 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 429 | 439 | <a href="/files/actions" tabindex="-1"> | |
| 430 | 440 | <span class="lbl">actions</span> | |
| 431 | 441 | <span class="k">section</span> | |
| 432 | 442 | </a> | |
| 433 | 443 | </li> | |
| 434 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 444 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 435 | 445 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 436 | 446 | <span class="lbl">feature/x</span> | |
| 437 | 447 | <span class="k">branch</span> | |
| 438 | 448 | </a> | |
| 439 | 449 | </li> | |
| 440 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 450 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> | |
| 441 | 451 | <a href="/files/tree/main" tabindex="-1"> | |
| 442 | 452 | <span class="lbl">main</span> | |
| 443 | 453 | <span class="k">branch</span> | |
| 444 | 454 | </a> | |
| 445 | 455 | </li> | |
| 446 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 456 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 447 | 457 | <a href="/files/tree/v1.0" tabindex="-1"> | |
| 448 | 458 | <span class="lbl">v1.0</span> | |
| 449 | 459 | <span class="k">tag</span> | |
| 450 | 460 | </a> | |
| 451 | 461 | </li> | |
| 452 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 462 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 453 | 463 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 454 | 464 | <span class="lbl">turn single-key shortcuts off</span> | |
| 455 | 465 | <span class="k">action</span> | |
| @@ -93,6 +93,10 @@ | |||||
| 93 | </a> | 93 | </a> | ||
| 94 | </li> | 94 | </li> | ||
| 95 | <li> | 95 | <li> | ||
| 96 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 97 | + | </a> | |||
| 98 | + | </li> | |||
| 99 | + | <li> | |||
| 96 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 100 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 97 | </a> | 101 | </a> | ||
| 98 | </li> | 102 | </li> | ||
| @@ -425,31 +429,37 @@ | |||||
| 425 | <span class="k">section</span> | 429 | <span class="k">section</span> | ||
| 426 | </a> | 430 | </a> | ||
| 427 | </li> | 431 | </li> | ||
| 428 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 432 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 433 | + | <a href="/files/releases" tabindex="-1"> | |||
| 434 | + | <span class="lbl">releases</span> | |||
| 435 | + | <span class="k">section</span> | |||
| 436 | + | </a> | |||
| 437 | + | </li> | |||
| 438 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 429 | <a href="/files/actions" tabindex="-1"> | 439 | <a href="/files/actions" tabindex="-1"> | ||
| 430 | <span class="lbl">actions</span> | 440 | <span class="lbl">actions</span> | ||
| 431 | <span class="k">section</span> | 441 | <span class="k">section</span> | ||
| 432 | </a> | 442 | </a> | ||
| 433 | </li> | 443 | </li> | ||
| 434 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | 444 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> |
| 435 | <a href="/files/tree/feature/x" tabindex="-1"> | 445 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 436 | <span class="lbl">feature/x</span> | 446 | <span class="lbl">feature/x</span> | ||
| 437 | <span class="k">branch</span> | 447 | <span class="k">branch</span> | ||
| 438 | </a> | 448 | </a> | ||
| 439 | </li> | 449 | </li> | ||
| 440 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | 450 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> |
| 441 | <a href="/files/tree/main" tabindex="-1"> | 451 | <a href="/files/tree/main" tabindex="-1"> | ||
| 442 | <span class="lbl">main</span> | 452 | <span class="lbl">main</span> | ||
| 443 | <span class="k">branch</span> | 453 | <span class="k">branch</span> | ||
| 444 | </a> | 454 | </a> | ||
| 445 | </li> | 455 | </li> | ||
| 446 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | 456 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> |
| 447 | <a href="/files/tree/v1.0" tabindex="-1"> | 457 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 448 | <span class="lbl">v1.0</span> | 458 | <span class="lbl">v1.0</span> | ||
| 449 | <span class="k">tag</span> | 459 | <span class="k">tag</span> | ||
| 450 | </a> | 460 | </a> | ||
| 451 | </li> | 461 | </li> | ||
| 452 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 462 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 453 | <a href="#" data-fire="keys" tabindex="-1"> | 463 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 454 | <span class="lbl">turn single-key shortcuts off</span> | 464 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 455 | <span class="k">action</span> | 465 | <span class="k">action</span> | ||
M internal/app/testdata/web/commit-big.html +15 -5
| @@ -92,6 +92,10 @@ | |||
| 92 | 92 | </a> | |
| 93 | 93 | </li> | |
| 94 | 94 | <li> | |
| 95 | + | <a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 96 | + | </a> | |
| 97 | + | </li> | |
| 98 | + | <li> | |
| 95 | 99 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 96 | 100 | </a> | |
| 97 | 101 | </li> | |
| @@ -410,31 +414,37 @@ | |||
| 410 | 414 | <span class="k">section</span> | |
| 411 | 415 | </a> | |
| 412 | 416 | </li> | |
| 413 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 417 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 418 | + | <a href="/diffs/releases" tabindex="-1"> | |
| 419 | + | <span class="lbl">releases</span> | |
| 420 | + | <span class="k">section</span> | |
| 421 | + | </a> | |
| 422 | + | </li> | |
| 423 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 414 | 424 | <a href="/diffs/actions" tabindex="-1"> | |
| 415 | 425 | <span class="lbl">actions</span> | |
| 416 | 426 | <span class="k">section</span> | |
| 417 | 427 | </a> | |
| 418 | 428 | </li> | |
| 419 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | |
| 429 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 420 | 430 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 421 | 431 | <span class="lbl">main</span> | |
| 422 | 432 | <span class="k">branch</span> | |
| 423 | 433 | </a> | |
| 424 | 434 | </li> | |
| 425 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch side"> | |
| 435 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side"> | |
| 426 | 436 | <a href="/diffs/tree/side" tabindex="-1"> | |
| 427 | 437 | <span class="lbl">side</span> | |
| 428 | 438 | <span class="k">branch</span> | |
| 429 | 439 | </a> | |
| 430 | 440 | </li> | |
| 431 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0"> | |
| 441 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0"> | |
| 432 | 442 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | |
| 433 | 443 | <span class="lbl">v1.0.0</span> | |
| 434 | 444 | <span class="k">tag</span> | |
| 435 | 445 | </a> | |
| 436 | 446 | </li> | |
| 437 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 447 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 438 | 448 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 439 | 449 | <span class="lbl">turn single-key shortcuts off</span> | |
| 440 | 450 | <span class="k">action</span> | |
| @@ -92,6 +92,10 @@ | |||||
| 92 | </a> | 92 | </a> | ||
| 93 | </li> | 93 | </li> | ||
| 94 | <li> | 94 | <li> | ||
| 95 | + | <a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 96 | + | </a> | |||
| 97 | + | </li> | |||
| 98 | + | <li> | |||
| 95 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 99 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 96 | </a> | 100 | </a> | ||
| 97 | </li> | 101 | </li> | ||
| @@ -410,31 +414,37 @@ | |||||
| 410 | <span class="k">section</span> | 414 | <span class="k">section</span> | ||
| 411 | </a> | 415 | </a> | ||
| 412 | </li> | 416 | </li> | ||
| 413 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 417 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 418 | + | <a href="/diffs/releases" tabindex="-1"> | |||
| 419 | + | <span class="lbl">releases</span> | |||
| 420 | + | <span class="k">section</span> | |||
| 421 | + | </a> | |||
| 422 | + | </li> | |||
| 423 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 414 | <a href="/diffs/actions" tabindex="-1"> | 424 | <a href="/diffs/actions" tabindex="-1"> | ||
| 415 | <span class="lbl">actions</span> | 425 | <span class="lbl">actions</span> | ||
| 416 | <span class="k">section</span> | 426 | <span class="k">section</span> | ||
| 417 | </a> | 427 | </a> | ||
| 418 | </li> | 428 | </li> | ||
| 419 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | 429 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> |
| 420 | <a href="/diffs/tree/main" tabindex="-1"> | 430 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 421 | <span class="lbl">main</span> | 431 | <span class="lbl">main</span> | ||
| 422 | <span class="k">branch</span> | 432 | <span class="k">branch</span> | ||
| 423 | </a> | 433 | </a> | ||
| 424 | </li> | 434 | </li> | ||
| 425 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch side"> | 435 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side"> |
| 426 | <a href="/diffs/tree/side" tabindex="-1"> | 436 | <a href="/diffs/tree/side" tabindex="-1"> | ||
| 427 | <span class="lbl">side</span> | 437 | <span class="lbl">side</span> | ||
| 428 | <span class="k">branch</span> | 438 | <span class="k">branch</span> | ||
| 429 | </a> | 439 | </a> | ||
| 430 | </li> | 440 | </li> | ||
| 431 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0"> | 441 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0"> |
| 432 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | 442 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | ||
| 433 | <span class="lbl">v1.0.0</span> | 443 | <span class="lbl">v1.0.0</span> | ||
| 434 | <span class="k">tag</span> | 444 | <span class="k">tag</span> | ||
| 435 | </a> | 445 | </a> | ||
| 436 | </li> | 446 | </li> | ||
| 437 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 447 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 438 | <a href="#" data-fire="keys" tabindex="-1"> | 448 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 439 | <span class="lbl">turn single-key shortcuts off</span> | 449 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 440 | <span class="k">action</span> | 450 | <span class="k">action</span> | ||
M internal/app/testdata/web/commit-binary.html +15 -5
| @@ -92,6 +92,10 @@ | |||
| 92 | 92 | </a> | |
| 93 | 93 | </li> | |
| 94 | 94 | <li> | |
| 95 | + | <a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 96 | + | </a> | |
| 97 | + | </li> | |
| 98 | + | <li> | |
| 95 | 99 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 96 | 100 | </a> | |
| 97 | 101 | </li> | |
| @@ -401,31 +405,37 @@ | |||
| 401 | 405 | <span class="k">section</span> | |
| 402 | 406 | </a> | |
| 403 | 407 | </li> | |
| 404 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 408 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 409 | + | <a href="/diffs/releases" tabindex="-1"> | |
| 410 | + | <span class="lbl">releases</span> | |
| 411 | + | <span class="k">section</span> | |
| 412 | + | </a> | |
| 413 | + | </li> | |
| 414 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 405 | 415 | <a href="/diffs/actions" tabindex="-1"> | |
| 406 | 416 | <span class="lbl">actions</span> | |
| 407 | 417 | <span class="k">section</span> | |
| 408 | 418 | </a> | |
| 409 | 419 | </li> | |
| 410 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | |
| 420 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 411 | 421 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 412 | 422 | <span class="lbl">main</span> | |
| 413 | 423 | <span class="k">branch</span> | |
| 414 | 424 | </a> | |
| 415 | 425 | </li> | |
| 416 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch side"> | |
| 426 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side"> | |
| 417 | 427 | <a href="/diffs/tree/side" tabindex="-1"> | |
| 418 | 428 | <span class="lbl">side</span> | |
| 419 | 429 | <span class="k">branch</span> | |
| 420 | 430 | </a> | |
| 421 | 431 | </li> | |
| 422 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0"> | |
| 432 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0"> | |
| 423 | 433 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | |
| 424 | 434 | <span class="lbl">v1.0.0</span> | |
| 425 | 435 | <span class="k">tag</span> | |
| 426 | 436 | </a> | |
| 427 | 437 | </li> | |
| 428 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 438 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 429 | 439 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 430 | 440 | <span class="lbl">turn single-key shortcuts off</span> | |
| 431 | 441 | <span class="k">action</span> | |
| @@ -92,6 +92,10 @@ | |||||
| 92 | </a> | 92 | </a> | ||
| 93 | </li> | 93 | </li> | ||
| 94 | <li> | 94 | <li> | ||
| 95 | + | <a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 96 | + | </a> | |||
| 97 | + | </li> | |||
| 98 | + | <li> | |||
| 95 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 99 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 96 | </a> | 100 | </a> | ||
| 97 | </li> | 101 | </li> | ||
| @@ -401,31 +405,37 @@ | |||||
| 401 | <span class="k">section</span> | 405 | <span class="k">section</span> | ||
| 402 | </a> | 406 | </a> | ||
| 403 | </li> | 407 | </li> | ||
| 404 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 408 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 409 | + | <a href="/diffs/releases" tabindex="-1"> | |||
| 410 | + | <span class="lbl">releases</span> | |||
| 411 | + | <span class="k">section</span> | |||
| 412 | + | </a> | |||
| 413 | + | </li> | |||
| 414 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 405 | <a href="/diffs/actions" tabindex="-1"> | 415 | <a href="/diffs/actions" tabindex="-1"> | ||
| 406 | <span class="lbl">actions</span> | 416 | <span class="lbl">actions</span> | ||
| 407 | <span class="k">section</span> | 417 | <span class="k">section</span> | ||
| 408 | </a> | 418 | </a> | ||
| 409 | </li> | 419 | </li> | ||
| 410 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | 420 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> |
| 411 | <a href="/diffs/tree/main" tabindex="-1"> | 421 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 412 | <span class="lbl">main</span> | 422 | <span class="lbl">main</span> | ||
| 413 | <span class="k">branch</span> | 423 | <span class="k">branch</span> | ||
| 414 | </a> | 424 | </a> | ||
| 415 | </li> | 425 | </li> | ||
| 416 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch side"> | 426 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side"> |
| 417 | <a href="/diffs/tree/side" tabindex="-1"> | 427 | <a href="/diffs/tree/side" tabindex="-1"> | ||
| 418 | <span class="lbl">side</span> | 428 | <span class="lbl">side</span> | ||
| 419 | <span class="k">branch</span> | 429 | <span class="k">branch</span> | ||
| 420 | </a> | 430 | </a> | ||
| 421 | </li> | 431 | </li> | ||
| 422 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0"> | 432 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0"> |
| 423 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | 433 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | ||
| 424 | <span class="lbl">v1.0.0</span> | 434 | <span class="lbl">v1.0.0</span> | ||
| 425 | <span class="k">tag</span> | 435 | <span class="k">tag</span> | ||
| 426 | </a> | 436 | </a> | ||
| 427 | </li> | 437 | </li> | ||
| 428 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 438 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 429 | <a href="#" data-fire="keys" tabindex="-1"> | 439 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 430 | <span class="lbl">turn single-key shortcuts off</span> | 440 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 431 | <span class="k">action</span> | 441 | <span class="k">action</span> | ||
M internal/app/testdata/web/commit-merge.html +15 -5
| @@ -92,6 +92,10 @@ | |||
| 92 | 92 | </a> | |
| 93 | 93 | </li> | |
| 94 | 94 | <li> | |
| 95 | + | <a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 96 | + | </a> | |
| 97 | + | </li> | |
| 98 | + | <li> | |
| 95 | 99 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 96 | 100 | </a> | |
| 97 | 101 | </li> | |
| @@ -443,31 +447,37 @@ | |||
| 443 | 447 | <span class="k">section</span> | |
| 444 | 448 | </a> | |
| 445 | 449 | </li> | |
| 446 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 450 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 451 | + | <a href="/diffs/releases" tabindex="-1"> | |
| 452 | + | <span class="lbl">releases</span> | |
| 453 | + | <span class="k">section</span> | |
| 454 | + | </a> | |
| 455 | + | </li> | |
| 456 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 447 | 457 | <a href="/diffs/actions" tabindex="-1"> | |
| 448 | 458 | <span class="lbl">actions</span> | |
| 449 | 459 | <span class="k">section</span> | |
| 450 | 460 | </a> | |
| 451 | 461 | </li> | |
| 452 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | |
| 462 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 453 | 463 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 454 | 464 | <span class="lbl">main</span> | |
| 455 | 465 | <span class="k">branch</span> | |
| 456 | 466 | </a> | |
| 457 | 467 | </li> | |
| 458 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch side"> | |
| 468 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side"> | |
| 459 | 469 | <a href="/diffs/tree/side" tabindex="-1"> | |
| 460 | 470 | <span class="lbl">side</span> | |
| 461 | 471 | <span class="k">branch</span> | |
| 462 | 472 | </a> | |
| 463 | 473 | </li> | |
| 464 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0"> | |
| 474 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0"> | |
| 465 | 475 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | |
| 466 | 476 | <span class="lbl">v1.0.0</span> | |
| 467 | 477 | <span class="k">tag</span> | |
| 468 | 478 | </a> | |
| 469 | 479 | </li> | |
| 470 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 480 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 471 | 481 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 472 | 482 | <span class="lbl">turn single-key shortcuts off</span> | |
| 473 | 483 | <span class="k">action</span> | |
| @@ -92,6 +92,10 @@ | |||||
| 92 | </a> | 92 | </a> | ||
| 93 | </li> | 93 | </li> | ||
| 94 | <li> | 94 | <li> | ||
| 95 | + | <a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 96 | + | </a> | |||
| 97 | + | </li> | |||
| 98 | + | <li> | |||
| 95 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 99 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 96 | </a> | 100 | </a> | ||
| 97 | </li> | 101 | </li> | ||
| @@ -443,31 +447,37 @@ | |||||
| 443 | <span class="k">section</span> | 447 | <span class="k">section</span> | ||
| 444 | </a> | 448 | </a> | ||
| 445 | </li> | 449 | </li> | ||
| 446 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 450 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 451 | + | <a href="/diffs/releases" tabindex="-1"> | |||
| 452 | + | <span class="lbl">releases</span> | |||
| 453 | + | <span class="k">section</span> | |||
| 454 | + | </a> | |||
| 455 | + | </li> | |||
| 456 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 447 | <a href="/diffs/actions" tabindex="-1"> | 457 | <a href="/diffs/actions" tabindex="-1"> | ||
| 448 | <span class="lbl">actions</span> | 458 | <span class="lbl">actions</span> | ||
| 449 | <span class="k">section</span> | 459 | <span class="k">section</span> | ||
| 450 | </a> | 460 | </a> | ||
| 451 | </li> | 461 | </li> | ||
| 452 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | 462 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> |
| 453 | <a href="/diffs/tree/main" tabindex="-1"> | 463 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 454 | <span class="lbl">main</span> | 464 | <span class="lbl">main</span> | ||
| 455 | <span class="k">branch</span> | 465 | <span class="k">branch</span> | ||
| 456 | </a> | 466 | </a> | ||
| 457 | </li> | 467 | </li> | ||
| 458 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch side"> | 468 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side"> |
| 459 | <a href="/diffs/tree/side" tabindex="-1"> | 469 | <a href="/diffs/tree/side" tabindex="-1"> | ||
| 460 | <span class="lbl">side</span> | 470 | <span class="lbl">side</span> | ||
| 461 | <span class="k">branch</span> | 471 | <span class="k">branch</span> | ||
| 462 | </a> | 472 | </a> | ||
| 463 | </li> | 473 | </li> | ||
| 464 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0"> | 474 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0"> |
| 465 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | 475 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | ||
| 466 | <span class="lbl">v1.0.0</span> | 476 | <span class="lbl">v1.0.0</span> | ||
| 467 | <span class="k">tag</span> | 477 | <span class="k">tag</span> | ||
| 468 | </a> | 478 | </a> | ||
| 469 | </li> | 479 | </li> | ||
| 470 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 480 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 471 | <a href="#" data-fire="keys" tabindex="-1"> | 481 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 472 | <span class="lbl">turn single-key shortcuts off</span> | 482 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 473 | <span class="k">action</span> | 483 | <span class="k">action</span> | ||
M internal/app/testdata/web/commit-modify-split.html +15 -5
| @@ -92,6 +92,10 @@ | |||
| 92 | 92 | </a> | |
| 93 | 93 | </li> | |
| 94 | 94 | <li> | |
| 95 | + | <a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 96 | + | </a> | |
| 97 | + | </li> | |
| 98 | + | <li> | |
| 95 | 99 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 96 | 100 | </a> | |
| 97 | 101 | </li> | |
| @@ -566,31 +570,37 @@ And <b>html</b> & "quotes".</pre> | |||
| 566 | 570 | <span class="k">section</span> | |
| 567 | 571 | </a> | |
| 568 | 572 | </li> | |
| 569 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 573 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 574 | + | <a href="/diffs/releases" tabindex="-1"> | |
| 575 | + | <span class="lbl">releases</span> | |
| 576 | + | <span class="k">section</span> | |
| 577 | + | </a> | |
| 578 | + | </li> | |
| 579 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 570 | 580 | <a href="/diffs/actions" tabindex="-1"> | |
| 571 | 581 | <span class="lbl">actions</span> | |
| 572 | 582 | <span class="k">section</span> | |
| 573 | 583 | </a> | |
| 574 | 584 | </li> | |
| 575 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | |
| 585 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 576 | 586 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 577 | 587 | <span class="lbl">main</span> | |
| 578 | 588 | <span class="k">branch</span> | |
| 579 | 589 | </a> | |
| 580 | 590 | </li> | |
| 581 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch side"> | |
| 591 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side"> | |
| 582 | 592 | <a href="/diffs/tree/side" tabindex="-1"> | |
| 583 | 593 | <span class="lbl">side</span> | |
| 584 | 594 | <span class="k">branch</span> | |
| 585 | 595 | </a> | |
| 586 | 596 | </li> | |
| 587 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0"> | |
| 597 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0"> | |
| 588 | 598 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | |
| 589 | 599 | <span class="lbl">v1.0.0</span> | |
| 590 | 600 | <span class="k">tag</span> | |
| 591 | 601 | </a> | |
| 592 | 602 | </li> | |
| 593 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 603 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 594 | 604 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 595 | 605 | <span class="lbl">turn single-key shortcuts off</span> | |
| 596 | 606 | <span class="k">action</span> | |
| @@ -92,6 +92,10 @@ | |||||
| 92 | </a> | 92 | </a> | ||
| 93 | </li> | 93 | </li> | ||
| 94 | <li> | 94 | <li> | ||
| 95 | + | <a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 96 | + | </a> | |||
| 97 | + | </li> | |||
| 98 | + | <li> | |||
| 95 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 99 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 96 | </a> | 100 | </a> | ||
| 97 | </li> | 101 | </li> | ||
| @@ -566,31 +570,37 @@ And <b>html</b> & "quotes".</pre> | |||||
| 566 | <span class="k">section</span> | 570 | <span class="k">section</span> | ||
| 567 | </a> | 571 | </a> | ||
| 568 | </li> | 572 | </li> | ||
| 569 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 573 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 574 | + | <a href="/diffs/releases" tabindex="-1"> | |||
| 575 | + | <span class="lbl">releases</span> | |||
| 576 | + | <span class="k">section</span> | |||
| 577 | + | </a> | |||
| 578 | + | </li> | |||
| 579 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 570 | <a href="/diffs/actions" tabindex="-1"> | 580 | <a href="/diffs/actions" tabindex="-1"> | ||
| 571 | <span class="lbl">actions</span> | 581 | <span class="lbl">actions</span> | ||
| 572 | <span class="k">section</span> | 582 | <span class="k">section</span> | ||
| 573 | </a> | 583 | </a> | ||
| 574 | </li> | 584 | </li> | ||
| 575 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | 585 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> |
| 576 | <a href="/diffs/tree/main" tabindex="-1"> | 586 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 577 | <span class="lbl">main</span> | 587 | <span class="lbl">main</span> | ||
| 578 | <span class="k">branch</span> | 588 | <span class="k">branch</span> | ||
| 579 | </a> | 589 | </a> | ||
| 580 | </li> | 590 | </li> | ||
| 581 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch side"> | 591 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side"> |
| 582 | <a href="/diffs/tree/side" tabindex="-1"> | 592 | <a href="/diffs/tree/side" tabindex="-1"> | ||
| 583 | <span class="lbl">side</span> | 593 | <span class="lbl">side</span> | ||
| 584 | <span class="k">branch</span> | 594 | <span class="k">branch</span> | ||
| 585 | </a> | 595 | </a> | ||
| 586 | </li> | 596 | </li> | ||
| 587 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0"> | 597 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0"> |
| 588 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | 598 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | ||
| 589 | <span class="lbl">v1.0.0</span> | 599 | <span class="lbl">v1.0.0</span> | ||
| 590 | <span class="k">tag</span> | 600 | <span class="k">tag</span> | ||
| 591 | </a> | 601 | </a> | ||
| 592 | </li> | 602 | </li> | ||
| 593 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 603 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 594 | <a href="#" data-fire="keys" tabindex="-1"> | 604 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 595 | <span class="lbl">turn single-key shortcuts off</span> | 605 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 596 | <span class="k">action</span> | 606 | <span class="k">action</span> | ||
M internal/app/testdata/web/commit-modify.html +15 -5
| @@ -92,6 +92,10 @@ | |||
| 92 | 92 | </a> | |
| 93 | 93 | </li> | |
| 94 | 94 | <li> | |
| 95 | + | <a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 96 | + | </a> | |
| 97 | + | </li> | |
| 98 | + | <li> | |
| 95 | 99 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 96 | 100 | </a> | |
| 97 | 101 | </li> | |
| @@ -566,31 +570,37 @@ And <b>html</b> & "quotes".</pre> | |||
| 566 | 570 | <span class="k">section</span> | |
| 567 | 571 | </a> | |
| 568 | 572 | </li> | |
| 569 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 573 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 574 | + | <a href="/diffs/releases" tabindex="-1"> | |
| 575 | + | <span class="lbl">releases</span> | |
| 576 | + | <span class="k">section</span> | |
| 577 | + | </a> | |
| 578 | + | </li> | |
| 579 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 570 | 580 | <a href="/diffs/actions" tabindex="-1"> | |
| 571 | 581 | <span class="lbl">actions</span> | |
| 572 | 582 | <span class="k">section</span> | |
| 573 | 583 | </a> | |
| 574 | 584 | </li> | |
| 575 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | |
| 585 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 576 | 586 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 577 | 587 | <span class="lbl">main</span> | |
| 578 | 588 | <span class="k">branch</span> | |
| 579 | 589 | </a> | |
| 580 | 590 | </li> | |
| 581 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch side"> | |
| 591 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side"> | |
| 582 | 592 | <a href="/diffs/tree/side" tabindex="-1"> | |
| 583 | 593 | <span class="lbl">side</span> | |
| 584 | 594 | <span class="k">branch</span> | |
| 585 | 595 | </a> | |
| 586 | 596 | </li> | |
| 587 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0"> | |
| 597 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0"> | |
| 588 | 598 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | |
| 589 | 599 | <span class="lbl">v1.0.0</span> | |
| 590 | 600 | <span class="k">tag</span> | |
| 591 | 601 | </a> | |
| 592 | 602 | </li> | |
| 593 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 603 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 594 | 604 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 595 | 605 | <span class="lbl">turn single-key shortcuts off</span> | |
| 596 | 606 | <span class="k">action</span> | |
| @@ -92,6 +92,10 @@ | |||||
| 92 | </a> | 92 | </a> | ||
| 93 | </li> | 93 | </li> | ||
| 94 | <li> | 94 | <li> | ||
| 95 | + | <a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 96 | + | </a> | |||
| 97 | + | </li> | |||
| 98 | + | <li> | |||
| 95 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 99 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 96 | </a> | 100 | </a> | ||
| 97 | </li> | 101 | </li> | ||
| @@ -566,31 +570,37 @@ And <b>html</b> & "quotes".</pre> | |||||
| 566 | <span class="k">section</span> | 570 | <span class="k">section</span> | ||
| 567 | </a> | 571 | </a> | ||
| 568 | </li> | 572 | </li> | ||
| 569 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 573 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 574 | + | <a href="/diffs/releases" tabindex="-1"> | |||
| 575 | + | <span class="lbl">releases</span> | |||
| 576 | + | <span class="k">section</span> | |||
| 577 | + | </a> | |||
| 578 | + | </li> | |||
| 579 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 570 | <a href="/diffs/actions" tabindex="-1"> | 580 | <a href="/diffs/actions" tabindex="-1"> | ||
| 571 | <span class="lbl">actions</span> | 581 | <span class="lbl">actions</span> | ||
| 572 | <span class="k">section</span> | 582 | <span class="k">section</span> | ||
| 573 | </a> | 583 | </a> | ||
| 574 | </li> | 584 | </li> | ||
| 575 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | 585 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> |
| 576 | <a href="/diffs/tree/main" tabindex="-1"> | 586 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 577 | <span class="lbl">main</span> | 587 | <span class="lbl">main</span> | ||
| 578 | <span class="k">branch</span> | 588 | <span class="k">branch</span> | ||
| 579 | </a> | 589 | </a> | ||
| 580 | </li> | 590 | </li> | ||
| 581 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch side"> | 591 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side"> |
| 582 | <a href="/diffs/tree/side" tabindex="-1"> | 592 | <a href="/diffs/tree/side" tabindex="-1"> | ||
| 583 | <span class="lbl">side</span> | 593 | <span class="lbl">side</span> | ||
| 584 | <span class="k">branch</span> | 594 | <span class="k">branch</span> | ||
| 585 | </a> | 595 | </a> | ||
| 586 | </li> | 596 | </li> | ||
| 587 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0"> | 597 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0"> |
| 588 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | 598 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | ||
| 589 | <span class="lbl">v1.0.0</span> | 599 | <span class="lbl">v1.0.0</span> | ||
| 590 | <span class="k">tag</span> | 600 | <span class="k">tag</span> | ||
| 591 | </a> | 601 | </a> | ||
| 592 | </li> | 602 | </li> | ||
| 593 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 603 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 594 | <a href="#" data-fire="keys" tabindex="-1"> | 604 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 595 | <span class="lbl">turn single-key shortcuts off</span> | 605 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 596 | <span class="k">action</span> | 606 | <span class="k">action</span> | ||
M internal/app/testdata/web/commit-rename.html +15 -5
| @@ -92,6 +92,10 @@ | |||
| 92 | 92 | </a> | |
| 93 | 93 | </li> | |
| 94 | 94 | <li> | |
| 95 | + | <a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 96 | + | </a> | |
| 97 | + | </li> | |
| 98 | + | <li> | |
| 95 | 99 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 96 | 100 | </a> | |
| 97 | 101 | </li> | |
| @@ -470,31 +474,37 @@ | |||
| 470 | 474 | <span class="k">section</span> | |
| 471 | 475 | </a> | |
| 472 | 476 | </li> | |
| 473 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 477 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 478 | + | <a href="/diffs/releases" tabindex="-1"> | |
| 479 | + | <span class="lbl">releases</span> | |
| 480 | + | <span class="k">section</span> | |
| 481 | + | </a> | |
| 482 | + | </li> | |
| 483 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 474 | 484 | <a href="/diffs/actions" tabindex="-1"> | |
| 475 | 485 | <span class="lbl">actions</span> | |
| 476 | 486 | <span class="k">section</span> | |
| 477 | 487 | </a> | |
| 478 | 488 | </li> | |
| 479 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | |
| 489 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 480 | 490 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 481 | 491 | <span class="lbl">main</span> | |
| 482 | 492 | <span class="k">branch</span> | |
| 483 | 493 | </a> | |
| 484 | 494 | </li> | |
| 485 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch side"> | |
| 495 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side"> | |
| 486 | 496 | <a href="/diffs/tree/side" tabindex="-1"> | |
| 487 | 497 | <span class="lbl">side</span> | |
| 488 | 498 | <span class="k">branch</span> | |
| 489 | 499 | </a> | |
| 490 | 500 | </li> | |
| 491 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0"> | |
| 501 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0"> | |
| 492 | 502 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | |
| 493 | 503 | <span class="lbl">v1.0.0</span> | |
| 494 | 504 | <span class="k">tag</span> | |
| 495 | 505 | </a> | |
| 496 | 506 | </li> | |
| 497 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 507 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 498 | 508 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 499 | 509 | <span class="lbl">turn single-key shortcuts off</span> | |
| 500 | 510 | <span class="k">action</span> | |
| @@ -92,6 +92,10 @@ | |||||
| 92 | </a> | 92 | </a> | ||
| 93 | </li> | 93 | </li> | ||
| 94 | <li> | 94 | <li> | ||
| 95 | + | <a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 96 | + | </a> | |||
| 97 | + | </li> | |||
| 98 | + | <li> | |||
| 95 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 99 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 96 | </a> | 100 | </a> | ||
| 97 | </li> | 101 | </li> | ||
| @@ -470,31 +474,37 @@ | |||||
| 470 | <span class="k">section</span> | 474 | <span class="k">section</span> | ||
| 471 | </a> | 475 | </a> | ||
| 472 | </li> | 476 | </li> | ||
| 473 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 477 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 478 | + | <a href="/diffs/releases" tabindex="-1"> | |||
| 479 | + | <span class="lbl">releases</span> | |||
| 480 | + | <span class="k">section</span> | |||
| 481 | + | </a> | |||
| 482 | + | </li> | |||
| 483 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 474 | <a href="/diffs/actions" tabindex="-1"> | 484 | <a href="/diffs/actions" tabindex="-1"> | ||
| 475 | <span class="lbl">actions</span> | 485 | <span class="lbl">actions</span> | ||
| 476 | <span class="k">section</span> | 486 | <span class="k">section</span> | ||
| 477 | </a> | 487 | </a> | ||
| 478 | </li> | 488 | </li> | ||
| 479 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | 489 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> |
| 480 | <a href="/diffs/tree/main" tabindex="-1"> | 490 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 481 | <span class="lbl">main</span> | 491 | <span class="lbl">main</span> | ||
| 482 | <span class="k">branch</span> | 492 | <span class="k">branch</span> | ||
| 483 | </a> | 493 | </a> | ||
| 484 | </li> | 494 | </li> | ||
| 485 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch side"> | 495 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side"> |
| 486 | <a href="/diffs/tree/side" tabindex="-1"> | 496 | <a href="/diffs/tree/side" tabindex="-1"> | ||
| 487 | <span class="lbl">side</span> | 497 | <span class="lbl">side</span> | ||
| 488 | <span class="k">branch</span> | 498 | <span class="k">branch</span> | ||
| 489 | </a> | 499 | </a> | ||
| 490 | </li> | 500 | </li> | ||
| 491 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0"> | 501 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0"> |
| 492 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | 502 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | ||
| 493 | <span class="lbl">v1.0.0</span> | 503 | <span class="lbl">v1.0.0</span> | ||
| 494 | <span class="k">tag</span> | 504 | <span class="k">tag</span> | ||
| 495 | </a> | 505 | </a> | ||
| 496 | </li> | 506 | </li> | ||
| 497 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 507 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 498 | <a href="#" data-fire="keys" tabindex="-1"> | 508 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 499 | <span class="lbl">turn single-key shortcuts off</span> | 509 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 500 | <span class="k">action</span> | 510 | <span class="k">action</span> | ||
M internal/app/testdata/web/commit-root.html +15 -5
| @@ -92,6 +92,10 @@ | |||
| 92 | 92 | </a> | |
| 93 | 93 | </li> | |
| 94 | 94 | <li> | |
| 95 | + | <a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 96 | + | </a> | |
| 97 | + | </li> | |
| 98 | + | <li> | |
| 95 | 99 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 96 | 100 | </a> | |
| 97 | 101 | </li> | |
| @@ -789,31 +793,37 @@ | |||
| 789 | 793 | <span class="k">section</span> | |
| 790 | 794 | </a> | |
| 791 | 795 | </li> | |
| 792 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 796 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 797 | + | <a href="/diffs/releases" tabindex="-1"> | |
| 798 | + | <span class="lbl">releases</span> | |
| 799 | + | <span class="k">section</span> | |
| 800 | + | </a> | |
| 801 | + | </li> | |
| 802 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 793 | 803 | <a href="/diffs/actions" tabindex="-1"> | |
| 794 | 804 | <span class="lbl">actions</span> | |
| 795 | 805 | <span class="k">section</span> | |
| 796 | 806 | </a> | |
| 797 | 807 | </li> | |
| 798 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | |
| 808 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 799 | 809 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 800 | 810 | <span class="lbl">main</span> | |
| 801 | 811 | <span class="k">branch</span> | |
| 802 | 812 | </a> | |
| 803 | 813 | </li> | |
| 804 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch side"> | |
| 814 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side"> | |
| 805 | 815 | <a href="/diffs/tree/side" tabindex="-1"> | |
| 806 | 816 | <span class="lbl">side</span> | |
| 807 | 817 | <span class="k">branch</span> | |
| 808 | 818 | </a> | |
| 809 | 819 | </li> | |
| 810 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0"> | |
| 820 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0"> | |
| 811 | 821 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | |
| 812 | 822 | <span class="lbl">v1.0.0</span> | |
| 813 | 823 | <span class="k">tag</span> | |
| 814 | 824 | </a> | |
| 815 | 825 | </li> | |
| 816 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 826 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 817 | 827 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 818 | 828 | <span class="lbl">turn single-key shortcuts off</span> | |
| 819 | 829 | <span class="k">action</span> | |
| @@ -92,6 +92,10 @@ | |||||
| 92 | </a> | 92 | </a> | ||
| 93 | </li> | 93 | </li> | ||
| 94 | <li> | 94 | <li> | ||
| 95 | + | <a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 96 | + | </a> | |||
| 97 | + | </li> | |||
| 98 | + | <li> | |||
| 95 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 99 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 96 | </a> | 100 | </a> | ||
| 97 | </li> | 101 | </li> | ||
| @@ -789,31 +793,37 @@ | |||||
| 789 | <span class="k">section</span> | 793 | <span class="k">section</span> | ||
| 790 | </a> | 794 | </a> | ||
| 791 | </li> | 795 | </li> | ||
| 792 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 796 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 797 | + | <a href="/diffs/releases" tabindex="-1"> | |||
| 798 | + | <span class="lbl">releases</span> | |||
| 799 | + | <span class="k">section</span> | |||
| 800 | + | </a> | |||
| 801 | + | </li> | |||
| 802 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 793 | <a href="/diffs/actions" tabindex="-1"> | 803 | <a href="/diffs/actions" tabindex="-1"> | ||
| 794 | <span class="lbl">actions</span> | 804 | <span class="lbl">actions</span> | ||
| 795 | <span class="k">section</span> | 805 | <span class="k">section</span> | ||
| 796 | </a> | 806 | </a> | ||
| 797 | </li> | 807 | </li> | ||
| 798 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | 808 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> |
| 799 | <a href="/diffs/tree/main" tabindex="-1"> | 809 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 800 | <span class="lbl">main</span> | 810 | <span class="lbl">main</span> | ||
| 801 | <span class="k">branch</span> | 811 | <span class="k">branch</span> | ||
| 802 | </a> | 812 | </a> | ||
| 803 | </li> | 813 | </li> | ||
| 804 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch side"> | 814 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side"> |
| 805 | <a href="/diffs/tree/side" tabindex="-1"> | 815 | <a href="/diffs/tree/side" tabindex="-1"> | ||
| 806 | <span class="lbl">side</span> | 816 | <span class="lbl">side</span> | ||
| 807 | <span class="k">branch</span> | 817 | <span class="k">branch</span> | ||
| 808 | </a> | 818 | </a> | ||
| 809 | </li> | 819 | </li> | ||
| 810 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0"> | 820 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0"> |
| 811 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | 821 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | ||
| 812 | <span class="lbl">v1.0.0</span> | 822 | <span class="lbl">v1.0.0</span> | ||
| 813 | <span class="k">tag</span> | 823 | <span class="k">tag</span> | ||
| 814 | </a> | 824 | </a> | ||
| 815 | </li> | 825 | </li> | ||
| 816 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 826 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 817 | <a href="#" data-fire="keys" tabindex="-1"> | 827 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 818 | <span class="lbl">turn single-key shortcuts off</span> | 828 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 819 | <span class="k">action</span> | 829 | <span class="k">action</span> | ||
M internal/app/testdata/web/commit-trojan.html +15 -5
| @@ -92,6 +92,10 @@ | |||
| 92 | 92 | </a> | |
| 93 | 93 | </li> | |
| 94 | 94 | <li> | |
| 95 | + | <a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 96 | + | </a> | |
| 97 | + | </li> | |
| 98 | + | <li> | |
| 95 | 99 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 96 | 100 | </a> | |
| 97 | 101 | </li> | |
| @@ -618,31 +622,37 @@ | |||
| 618 | 622 | <span class="k">section</span> | |
| 619 | 623 | </a> | |
| 620 | 624 | </li> | |
| 621 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 625 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 626 | + | <a href="/diffs/releases" tabindex="-1"> | |
| 627 | + | <span class="lbl">releases</span> | |
| 628 | + | <span class="k">section</span> | |
| 629 | + | </a> | |
| 630 | + | </li> | |
| 631 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 622 | 632 | <a href="/diffs/actions" tabindex="-1"> | |
| 623 | 633 | <span class="lbl">actions</span> | |
| 624 | 634 | <span class="k">section</span> | |
| 625 | 635 | </a> | |
| 626 | 636 | </li> | |
| 627 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | |
| 637 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 628 | 638 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 629 | 639 | <span class="lbl">main</span> | |
| 630 | 640 | <span class="k">branch</span> | |
| 631 | 641 | </a> | |
| 632 | 642 | </li> | |
| 633 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch side"> | |
| 643 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side"> | |
| 634 | 644 | <a href="/diffs/tree/side" tabindex="-1"> | |
| 635 | 645 | <span class="lbl">side</span> | |
| 636 | 646 | <span class="k">branch</span> | |
| 637 | 647 | </a> | |
| 638 | 648 | </li> | |
| 639 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0"> | |
| 649 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0"> | |
| 640 | 650 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | |
| 641 | 651 | <span class="lbl">v1.0.0</span> | |
| 642 | 652 | <span class="k">tag</span> | |
| 643 | 653 | </a> | |
| 644 | 654 | </li> | |
| 645 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 655 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 646 | 656 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 647 | 657 | <span class="lbl">turn single-key shortcuts off</span> | |
| 648 | 658 | <span class="k">action</span> | |
| @@ -92,6 +92,10 @@ | |||||
| 92 | </a> | 92 | </a> | ||
| 93 | </li> | 93 | </li> | ||
| 94 | <li> | 94 | <li> | ||
| 95 | + | <a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 96 | + | </a> | |||
| 97 | + | </li> | |||
| 98 | + | <li> | |||
| 95 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 99 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 96 | </a> | 100 | </a> | ||
| 97 | </li> | 101 | </li> | ||
| @@ -618,31 +622,37 @@ | |||||
| 618 | <span class="k">section</span> | 622 | <span class="k">section</span> | ||
| 619 | </a> | 623 | </a> | ||
| 620 | </li> | 624 | </li> | ||
| 621 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 625 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 626 | + | <a href="/diffs/releases" tabindex="-1"> | |||
| 627 | + | <span class="lbl">releases</span> | |||
| 628 | + | <span class="k">section</span> | |||
| 629 | + | </a> | |||
| 630 | + | </li> | |||
| 631 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 622 | <a href="/diffs/actions" tabindex="-1"> | 632 | <a href="/diffs/actions" tabindex="-1"> | ||
| 623 | <span class="lbl">actions</span> | 633 | <span class="lbl">actions</span> | ||
| 624 | <span class="k">section</span> | 634 | <span class="k">section</span> | ||
| 625 | </a> | 635 | </a> | ||
| 626 | </li> | 636 | </li> | ||
| 627 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | 637 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> |
| 628 | <a href="/diffs/tree/main" tabindex="-1"> | 638 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 629 | <span class="lbl">main</span> | 639 | <span class="lbl">main</span> | ||
| 630 | <span class="k">branch</span> | 640 | <span class="k">branch</span> | ||
| 631 | </a> | 641 | </a> | ||
| 632 | </li> | 642 | </li> | ||
| 633 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch side"> | 643 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side"> |
| 634 | <a href="/diffs/tree/side" tabindex="-1"> | 644 | <a href="/diffs/tree/side" tabindex="-1"> | ||
| 635 | <span class="lbl">side</span> | 645 | <span class="lbl">side</span> | ||
| 636 | <span class="k">branch</span> | 646 | <span class="k">branch</span> | ||
| 637 | </a> | 647 | </a> | ||
| 638 | </li> | 648 | </li> | ||
| 639 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0"> | 649 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0"> |
| 640 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | 650 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | ||
| 641 | <span class="lbl">v1.0.0</span> | 651 | <span class="lbl">v1.0.0</span> | ||
| 642 | <span class="k">tag</span> | 652 | <span class="k">tag</span> | ||
| 643 | </a> | 653 | </a> | ||
| 644 | </li> | 654 | </li> | ||
| 645 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 655 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 646 | <a href="#" data-fire="keys" tabindex="-1"> | 656 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 647 | <span class="lbl">turn single-key shortcuts off</span> | 657 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 648 | <span class="k">action</span> | 658 | <span class="k">action</span> | ||
M internal/app/testdata/web/commits-page2.html +14 -4
| @@ -90,6 +90,10 @@ | |||
| 90 | 90 | </a> | |
| 91 | 91 | </li> | |
| 92 | 92 | <li> | |
| 93 | + | <a href="/long/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 94 | + | </a> | |
| 95 | + | </li> | |
| 96 | + | <li> | |
| 93 | 97 | <a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 94 | 98 | </a> | |
| 95 | 99 | </li> | |
| @@ -377,25 +381,31 @@ | |||
| 377 | 381 | <span class="k">section</span> | |
| 378 | 382 | </a> | |
| 379 | 383 | </li> | |
| 380 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 384 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 385 | + | <a href="/long/releases" tabindex="-1"> | |
| 386 | + | <span class="lbl">releases</span> | |
| 387 | + | <span class="k">section</span> | |
| 388 | + | </a> | |
| 389 | + | </li> | |
| 390 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 381 | 391 | <a href="/long/actions" tabindex="-1"> | |
| 382 | 392 | <span class="lbl">actions</span> | |
| 383 | 393 | <span class="k">section</span> | |
| 384 | 394 | </a> | |
| 385 | 395 | </li> | |
| 386 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | |
| 396 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 387 | 397 | <a href="/long/tree/main" tabindex="-1"> | |
| 388 | 398 | <span class="lbl">main</span> | |
| 389 | 399 | <span class="k">branch</span> | |
| 390 | 400 | </a> | |
| 391 | 401 | </li> | |
| 392 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="repo diffs"> | |
| 402 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="repo diffs"> | |
| 393 | 403 | <a href="/diffs" tabindex="-1"> | |
| 394 | 404 | <span class="lbl">diffs</span> | |
| 395 | 405 | <span class="k">repo</span> | |
| 396 | 406 | </a> | |
| 397 | 407 | </li> | |
| 398 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 408 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 399 | 409 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 400 | 410 | <span class="lbl">turn single-key shortcuts off</span> | |
| 401 | 411 | <span class="k">action</span> | |
| @@ -90,6 +90,10 @@ | |||||
| 90 | </a> | 90 | </a> | ||
| 91 | </li> | 91 | </li> | ||
| 92 | <li> | 92 | <li> | ||
| 93 | + | <a href="/long/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 94 | + | </a> | |||
| 95 | + | </li> | |||
| 96 | + | <li> | |||
| 93 | <a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 97 | <a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 94 | </a> | 98 | </a> | ||
| 95 | </li> | 99 | </li> | ||
| @@ -377,25 +381,31 @@ | |||||
| 377 | <span class="k">section</span> | 381 | <span class="k">section</span> | ||
| 378 | </a> | 382 | </a> | ||
| 379 | </li> | 383 | </li> | ||
| 380 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 384 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 385 | + | <a href="/long/releases" tabindex="-1"> | |||
| 386 | + | <span class="lbl">releases</span> | |||
| 387 | + | <span class="k">section</span> | |||
| 388 | + | </a> | |||
| 389 | + | </li> | |||
| 390 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 381 | <a href="/long/actions" tabindex="-1"> | 391 | <a href="/long/actions" tabindex="-1"> | ||
| 382 | <span class="lbl">actions</span> | 392 | <span class="lbl">actions</span> | ||
| 383 | <span class="k">section</span> | 393 | <span class="k">section</span> | ||
| 384 | </a> | 394 | </a> | ||
| 385 | </li> | 395 | </li> | ||
| 386 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | 396 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> |
| 387 | <a href="/long/tree/main" tabindex="-1"> | 397 | <a href="/long/tree/main" tabindex="-1"> | ||
| 388 | <span class="lbl">main</span> | 398 | <span class="lbl">main</span> | ||
| 389 | <span class="k">branch</span> | 399 | <span class="k">branch</span> | ||
| 390 | </a> | 400 | </a> | ||
| 391 | </li> | 401 | </li> | ||
| 392 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="repo diffs"> | 402 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="repo diffs"> |
| 393 | <a href="/diffs" tabindex="-1"> | 403 | <a href="/diffs" tabindex="-1"> | ||
| 394 | <span class="lbl">diffs</span> | 404 | <span class="lbl">diffs</span> | ||
| 395 | <span class="k">repo</span> | 405 | <span class="k">repo</span> | ||
| 396 | </a> | 406 | </a> | ||
| 397 | </li> | 407 | </li> | ||
| 398 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 408 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 399 | <a href="#" data-fire="keys" tabindex="-1"> | 409 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 400 | <span class="lbl">turn single-key shortcuts off</span> | 410 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 401 | <span class="k">action</span> | 411 | <span class="k">action</span> | ||
M internal/app/testdata/web/commits-path.html +14 -4
| @@ -93,6 +93,10 @@ | |||
| 93 | 93 | </a> | |
| 94 | 94 | </li> | |
| 95 | 95 | <li> | |
| 96 | + | <a href="/long/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 97 | + | </a> | |
| 98 | + | </li> | |
| 99 | + | <li> | |
| 96 | 100 | <a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 97 | 101 | </a> | |
| 98 | 102 | </li> | |
| @@ -392,25 +396,31 @@ | |||
| 392 | 396 | <span class="k">section</span> | |
| 393 | 397 | </a> | |
| 394 | 398 | </li> | |
| 395 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 399 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 400 | + | <a href="/long/releases" tabindex="-1"> | |
| 401 | + | <span class="lbl">releases</span> | |
| 402 | + | <span class="k">section</span> | |
| 403 | + | </a> | |
| 404 | + | </li> | |
| 405 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 396 | 406 | <a href="/long/actions" tabindex="-1"> | |
| 397 | 407 | <span class="lbl">actions</span> | |
| 398 | 408 | <span class="k">section</span> | |
| 399 | 409 | </a> | |
| 400 | 410 | </li> | |
| 401 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | |
| 411 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 402 | 412 | <a href="/long/tree/main" tabindex="-1"> | |
| 403 | 413 | <span class="lbl">main</span> | |
| 404 | 414 | <span class="k">branch</span> | |
| 405 | 415 | </a> | |
| 406 | 416 | </li> | |
| 407 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="repo diffs"> | |
| 417 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="repo diffs"> | |
| 408 | 418 | <a href="/diffs" tabindex="-1"> | |
| 409 | 419 | <span class="lbl">diffs</span> | |
| 410 | 420 | <span class="k">repo</span> | |
| 411 | 421 | </a> | |
| 412 | 422 | </li> | |
| 413 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 423 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 414 | 424 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 415 | 425 | <span class="lbl">turn single-key shortcuts off</span> | |
| 416 | 426 | <span class="k">action</span> | |
| @@ -93,6 +93,10 @@ | |||||
| 93 | </a> | 93 | </a> | ||
| 94 | </li> | 94 | </li> | ||
| 95 | <li> | 95 | <li> | ||
| 96 | + | <a href="/long/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 97 | + | </a> | |||
| 98 | + | </li> | |||
| 99 | + | <li> | |||
| 96 | <a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 100 | <a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 97 | </a> | 101 | </a> | ||
| 98 | </li> | 102 | </li> | ||
| @@ -392,25 +396,31 @@ | |||||
| 392 | <span class="k">section</span> | 396 | <span class="k">section</span> | ||
| 393 | </a> | 397 | </a> | ||
| 394 | </li> | 398 | </li> | ||
| 395 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 399 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 400 | + | <a href="/long/releases" tabindex="-1"> | |||
| 401 | + | <span class="lbl">releases</span> | |||
| 402 | + | <span class="k">section</span> | |||
| 403 | + | </a> | |||
| 404 | + | </li> | |||
| 405 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 396 | <a href="/long/actions" tabindex="-1"> | 406 | <a href="/long/actions" tabindex="-1"> | ||
| 397 | <span class="lbl">actions</span> | 407 | <span class="lbl">actions</span> | ||
| 398 | <span class="k">section</span> | 408 | <span class="k">section</span> | ||
| 399 | </a> | 409 | </a> | ||
| 400 | </li> | 410 | </li> | ||
| 401 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | 411 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> |
| 402 | <a href="/long/tree/main" tabindex="-1"> | 412 | <a href="/long/tree/main" tabindex="-1"> | ||
| 403 | <span class="lbl">main</span> | 413 | <span class="lbl">main</span> | ||
| 404 | <span class="k">branch</span> | 414 | <span class="k">branch</span> | ||
| 405 | </a> | 415 | </a> | ||
| 406 | </li> | 416 | </li> | ||
| 407 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="repo diffs"> | 417 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="repo diffs"> |
| 408 | <a href="/diffs" tabindex="-1"> | 418 | <a href="/diffs" tabindex="-1"> | ||
| 409 | <span class="lbl">diffs</span> | 419 | <span class="lbl">diffs</span> | ||
| 410 | <span class="k">repo</span> | 420 | <span class="k">repo</span> | ||
| 411 | </a> | 421 | </a> | ||
| 412 | </li> | 422 | </li> | ||
| 413 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 423 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 414 | <a href="#" data-fire="keys" tabindex="-1"> | 424 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 415 | <span class="lbl">turn single-key shortcuts off</span> | 425 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 416 | <span class="k">action</span> | 426 | <span class="k">action</span> | ||
M internal/app/testdata/web/commits-root.html +14 -4
| @@ -90,6 +90,10 @@ | |||
| 90 | 90 | </a> | |
| 91 | 91 | </li> | |
| 92 | 92 | <li> | |
| 93 | + | <a href="/long/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 94 | + | </a> | |
| 95 | + | </li> | |
| 96 | + | <li> | |
| 93 | 97 | <a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 94 | 98 | </a> | |
| 95 | 99 | </li> | |
| @@ -1182,25 +1186,31 @@ | |||
| 1182 | 1186 | <span class="k">section</span> | |
| 1183 | 1187 | </a> | |
| 1184 | 1188 | </li> | |
| 1185 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 1189 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 1190 | + | <a href="/long/releases" tabindex="-1"> | |
| 1191 | + | <span class="lbl">releases</span> | |
| 1192 | + | <span class="k">section</span> | |
| 1193 | + | </a> | |
| 1194 | + | </li> | |
| 1195 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 1186 | 1196 | <a href="/long/actions" tabindex="-1"> | |
| 1187 | 1197 | <span class="lbl">actions</span> | |
| 1188 | 1198 | <span class="k">section</span> | |
| 1189 | 1199 | </a> | |
| 1190 | 1200 | </li> | |
| 1191 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | |
| 1201 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 1192 | 1202 | <a href="/long/tree/main" tabindex="-1"> | |
| 1193 | 1203 | <span class="lbl">main</span> | |
| 1194 | 1204 | <span class="k">branch</span> | |
| 1195 | 1205 | </a> | |
| 1196 | 1206 | </li> | |
| 1197 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="repo diffs"> | |
| 1207 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="repo diffs"> | |
| 1198 | 1208 | <a href="/diffs" tabindex="-1"> | |
| 1199 | 1209 | <span class="lbl">diffs</span> | |
| 1200 | 1210 | <span class="k">repo</span> | |
| 1201 | 1211 | </a> | |
| 1202 | 1212 | </li> | |
| 1203 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 1213 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 1204 | 1214 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 1205 | 1215 | <span class="lbl">turn single-key shortcuts off</span> | |
| 1206 | 1216 | <span class="k">action</span> | |
| @@ -90,6 +90,10 @@ | |||||
| 90 | </a> | 90 | </a> | ||
| 91 | </li> | 91 | </li> | ||
| 92 | <li> | 92 | <li> | ||
| 93 | + | <a href="/long/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 94 | + | </a> | |||
| 95 | + | </li> | |||
| 96 | + | <li> | |||
| 93 | <a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 97 | <a href="/long/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 94 | </a> | 98 | </a> | ||
| 95 | </li> | 99 | </li> | ||
| @@ -1182,25 +1186,31 @@ | |||||
| 1182 | <span class="k">section</span> | 1186 | <span class="k">section</span> | ||
| 1183 | </a> | 1187 | </a> | ||
| 1184 | </li> | 1188 | </li> | ||
| 1185 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 1189 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 1190 | + | <a href="/long/releases" tabindex="-1"> | |||
| 1191 | + | <span class="lbl">releases</span> | |||
| 1192 | + | <span class="k">section</span> | |||
| 1193 | + | </a> | |||
| 1194 | + | </li> | |||
| 1195 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 1186 | <a href="/long/actions" tabindex="-1"> | 1196 | <a href="/long/actions" tabindex="-1"> | ||
| 1187 | <span class="lbl">actions</span> | 1197 | <span class="lbl">actions</span> | ||
| 1188 | <span class="k">section</span> | 1198 | <span class="k">section</span> | ||
| 1189 | </a> | 1199 | </a> | ||
| 1190 | </li> | 1200 | </li> | ||
| 1191 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | 1201 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> |
| 1192 | <a href="/long/tree/main" tabindex="-1"> | 1202 | <a href="/long/tree/main" tabindex="-1"> | ||
| 1193 | <span class="lbl">main</span> | 1203 | <span class="lbl">main</span> | ||
| 1194 | <span class="k">branch</span> | 1204 | <span class="k">branch</span> | ||
| 1195 | </a> | 1205 | </a> | ||
| 1196 | </li> | 1206 | </li> | ||
| 1197 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="repo diffs"> | 1207 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="repo diffs"> |
| 1198 | <a href="/diffs" tabindex="-1"> | 1208 | <a href="/diffs" tabindex="-1"> | ||
| 1199 | <span class="lbl">diffs</span> | 1209 | <span class="lbl">diffs</span> | ||
| 1200 | <span class="k">repo</span> | 1210 | <span class="k">repo</span> | ||
| 1201 | </a> | 1211 | </a> | ||
| 1202 | </li> | 1212 | </li> | ||
| 1203 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 1213 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 1204 | <a href="#" data-fire="keys" tabindex="-1"> | 1214 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 1205 | <span class="lbl">turn single-key shortcuts off</span> | 1215 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 1206 | <span class="k">action</span> | 1216 | <span class="k">action</span> | ||
M internal/app/testdata/web/commits-small.html +16 -6
| @@ -90,6 +90,10 @@ | |||
| 90 | 90 | </a> | |
| 91 | 91 | </li> | |
| 92 | 92 | <li> | |
| 93 | + | <a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 94 | + | </a> | |
| 95 | + | </li> | |
| 96 | + | <li> | |
| 93 | 97 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 94 | 98 | </a> | |
| 95 | 99 | </li> | |
| @@ -535,37 +539,43 @@ | |||
| 535 | 539 | <span class="k">section</span> | |
| 536 | 540 | </a> | |
| 537 | 541 | </li> | |
| 538 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 542 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 543 | + | <a href="/diffs/releases" tabindex="-1"> | |
| 544 | + | <span class="lbl">releases</span> | |
| 545 | + | <span class="k">section</span> | |
| 546 | + | </a> | |
| 547 | + | </li> | |
| 548 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 539 | 549 | <a href="/diffs/actions" tabindex="-1"> | |
| 540 | 550 | <span class="lbl">actions</span> | |
| 541 | 551 | <span class="k">section</span> | |
| 542 | 552 | </a> | |
| 543 | 553 | </li> | |
| 544 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | |
| 554 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 545 | 555 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 546 | 556 | <span class="lbl">main</span> | |
| 547 | 557 | <span class="k">branch</span> | |
| 548 | 558 | </a> | |
| 549 | 559 | </li> | |
| 550 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch side"> | |
| 560 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side"> | |
| 551 | 561 | <a href="/diffs/tree/side" tabindex="-1"> | |
| 552 | 562 | <span class="lbl">side</span> | |
| 553 | 563 | <span class="k">branch</span> | |
| 554 | 564 | </a> | |
| 555 | 565 | </li> | |
| 556 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0"> | |
| 566 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0"> | |
| 557 | 567 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | |
| 558 | 568 | <span class="lbl">v1.0.0</span> | |
| 559 | 569 | <span class="k">tag</span> | |
| 560 | 570 | </a> | |
| 561 | 571 | </li> | |
| 562 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="repo long"> | |
| 572 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="repo long"> | |
| 563 | 573 | <a href="/long" tabindex="-1"> | |
| 564 | 574 | <span class="lbl">long</span> | |
| 565 | 575 | <span class="k">repo</span> | |
| 566 | 576 | </a> | |
| 567 | 577 | </li> | |
| 568 | - | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 578 | + | <li id="cmd-o12" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 569 | 579 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 570 | 580 | <span class="lbl">turn single-key shortcuts off</span> | |
| 571 | 581 | <span class="k">action</span> | |
| @@ -90,6 +90,10 @@ | |||||
| 90 | </a> | 90 | </a> | ||
| 91 | </li> | 91 | </li> | ||
| 92 | <li> | 92 | <li> | ||
| 93 | + | <a href="/diffs/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 94 | + | </a> | |||
| 95 | + | </li> | |||
| 96 | + | <li> | |||
| 93 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 97 | <a href="/diffs/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 94 | </a> | 98 | </a> | ||
| 95 | </li> | 99 | </li> | ||
| @@ -535,37 +539,43 @@ | |||||
| 535 | <span class="k">section</span> | 539 | <span class="k">section</span> | ||
| 536 | </a> | 540 | </a> | ||
| 537 | </li> | 541 | </li> | ||
| 538 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 542 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 543 | + | <a href="/diffs/releases" tabindex="-1"> | |||
| 544 | + | <span class="lbl">releases</span> | |||
| 545 | + | <span class="k">section</span> | |||
| 546 | + | </a> | |||
| 547 | + | </li> | |||
| 548 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 539 | <a href="/diffs/actions" tabindex="-1"> | 549 | <a href="/diffs/actions" tabindex="-1"> | ||
| 540 | <span class="lbl">actions</span> | 550 | <span class="lbl">actions</span> | ||
| 541 | <span class="k">section</span> | 551 | <span class="k">section</span> | ||
| 542 | </a> | 552 | </a> | ||
| 543 | </li> | 553 | </li> | ||
| 544 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch main"> | 554 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> |
| 545 | <a href="/diffs/tree/main" tabindex="-1"> | 555 | <a href="/diffs/tree/main" tabindex="-1"> | ||
| 546 | <span class="lbl">main</span> | 556 | <span class="lbl">main</span> | ||
| 547 | <span class="k">branch</span> | 557 | <span class="k">branch</span> | ||
| 548 | </a> | 558 | </a> | ||
| 549 | </li> | 559 | </li> | ||
| 550 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch side"> | 560 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch side"> |
| 551 | <a href="/diffs/tree/side" tabindex="-1"> | 561 | <a href="/diffs/tree/side" tabindex="-1"> | ||
| 552 | <span class="lbl">side</span> | 562 | <span class="lbl">side</span> | ||
| 553 | <span class="k">branch</span> | 563 | <span class="k">branch</span> | ||
| 554 | </a> | 564 | </a> | ||
| 555 | </li> | 565 | </li> | ||
| 556 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0.0"> | 566 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0.0"> |
| 557 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | 567 | <a href="/diffs/tree/v1.0.0" tabindex="-1"> | ||
| 558 | <span class="lbl">v1.0.0</span> | 568 | <span class="lbl">v1.0.0</span> | ||
| 559 | <span class="k">tag</span> | 569 | <span class="k">tag</span> | ||
| 560 | </a> | 570 | </a> | ||
| 561 | </li> | 571 | </li> | ||
| 562 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="repo long"> | 572 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="repo long"> |
| 563 | <a href="/long" tabindex="-1"> | 573 | <a href="/long" tabindex="-1"> | ||
| 564 | <span class="lbl">long</span> | 574 | <span class="lbl">long</span> | ||
| 565 | <span class="k">repo</span> | 575 | <span class="k">repo</span> | ||
| 566 | </a> | 576 | </a> | ||
| 567 | </li> | 577 | </li> | ||
| 568 | - | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 578 | + | <li id="cmd-o12" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 569 | <a href="#" data-fire="keys" tabindex="-1"> | 579 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 570 | <span class="lbl">turn single-key shortcuts off</span> | 580 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 571 | <span class="k">action</span> | 581 | <span class="k">action</span> | ||
M internal/app/testdata/web/refs.html +17 -7
| @@ -87,6 +87,10 @@ | |||
| 87 | 87 | </a> | |
| 88 | 88 | </li> | |
| 89 | 89 | <li> | |
| 90 | + | <a href="/demo/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 91 | + | </a> | |
| 92 | + | </li> | |
| 93 | + | <li> | |
| 90 | 94 | <a href="/demo/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 91 | 95 | </a> | |
| 92 | 96 | </li> | |
| @@ -421,43 +425,49 @@ | |||
| 421 | 425 | <span class="k">section</span> | |
| 422 | 426 | </a> | |
| 423 | 427 | </li> | |
| 424 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 428 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 429 | + | <a href="/demo/releases" tabindex="-1"> | |
| 430 | + | <span class="lbl">releases</span> | |
| 431 | + | <span class="k">section</span> | |
| 432 | + | </a> | |
| 433 | + | </li> | |
| 434 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 425 | 435 | <a href="/demo/actions" tabindex="-1"> | |
| 426 | 436 | <span class="lbl">actions</span> | |
| 427 | 437 | <span class="k">section</span> | |
| 428 | 438 | </a> | |
| 429 | 439 | </li> | |
| 430 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 440 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 431 | 441 | <a href="/demo/tree/feature/x" tabindex="-1"> | |
| 432 | 442 | <span class="lbl">feature/x</span> | |
| 433 | 443 | <span class="k">branch</span> | |
| 434 | 444 | </a> | |
| 435 | 445 | </li> | |
| 436 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 446 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> | |
| 437 | 447 | <a href="/demo/tree/main" tabindex="-1"> | |
| 438 | 448 | <span class="lbl">main</span> | |
| 439 | 449 | <span class="k">branch</span> | |
| 440 | 450 | </a> | |
| 441 | 451 | </li> | |
| 442 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v0.1.0"> | |
| 452 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v0.1.0"> | |
| 443 | 453 | <a href="/demo/tree/v0.1.0" tabindex="-1"> | |
| 444 | 454 | <span class="lbl">v0.1.0</span> | |
| 445 | 455 | <span class="k">tag</span> | |
| 446 | 456 | </a> | |
| 447 | 457 | </li> | |
| 448 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v0.2.0"> | |
| 458 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="tag v0.2.0"> | |
| 449 | 459 | <a href="/demo/tree/v0.2.0" tabindex="-1"> | |
| 450 | 460 | <span class="lbl">v0.2.0</span> | |
| 451 | 461 | <span class="k">tag</span> | |
| 452 | 462 | </a> | |
| 453 | 463 | </li> | |
| 454 | - | <li id="cmd-o11" role="option" aria-selected="false" data-k="repo quiet"> | |
| 464 | + | <li id="cmd-o12" role="option" aria-selected="false" data-k="repo quiet"> | |
| 455 | 465 | <a href="/quiet" tabindex="-1"> | |
| 456 | 466 | <span class="lbl">quiet</span> | |
| 457 | 467 | <span class="k">repo</span> | |
| 458 | 468 | </a> | |
| 459 | 469 | </li> | |
| 460 | - | <li id="cmd-o12" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 470 | + | <li id="cmd-o13" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 461 | 471 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 462 | 472 | <span class="lbl">turn single-key shortcuts off</span> | |
| 463 | 473 | <span class="k">action</span> | |
| @@ -87,6 +87,10 @@ | |||||
| 87 | </a> | 87 | </a> | ||
| 88 | </li> | 88 | </li> | ||
| 89 | <li> | 89 | <li> | ||
| 90 | + | <a href="/demo/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 91 | + | </a> | |||
| 92 | + | </li> | |||
| 93 | + | <li> | |||
| 90 | <a href="/demo/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 94 | <a href="/demo/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 91 | </a> | 95 | </a> | ||
| 92 | </li> | 96 | </li> | ||
| @@ -421,43 +425,49 @@ | |||||
| 421 | <span class="k">section</span> | 425 | <span class="k">section</span> | ||
| 422 | </a> | 426 | </a> | ||
| 423 | </li> | 427 | </li> | ||
| 424 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 428 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 429 | + | <a href="/demo/releases" tabindex="-1"> | |||
| 430 | + | <span class="lbl">releases</span> | |||
| 431 | + | <span class="k">section</span> | |||
| 432 | + | </a> | |||
| 433 | + | </li> | |||
| 434 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 425 | <a href="/demo/actions" tabindex="-1"> | 435 | <a href="/demo/actions" tabindex="-1"> | ||
| 426 | <span class="lbl">actions</span> | 436 | <span class="lbl">actions</span> | ||
| 427 | <span class="k">section</span> | 437 | <span class="k">section</span> | ||
| 428 | </a> | 438 | </a> | ||
| 429 | </li> | 439 | </li> | ||
| 430 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | 440 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> |
| 431 | <a href="/demo/tree/feature/x" tabindex="-1"> | 441 | <a href="/demo/tree/feature/x" tabindex="-1"> | ||
| 432 | <span class="lbl">feature/x</span> | 442 | <span class="lbl">feature/x</span> | ||
| 433 | <span class="k">branch</span> | 443 | <span class="k">branch</span> | ||
| 434 | </a> | 444 | </a> | ||
| 435 | </li> | 445 | </li> | ||
| 436 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | 446 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> |
| 437 | <a href="/demo/tree/main" tabindex="-1"> | 447 | <a href="/demo/tree/main" tabindex="-1"> | ||
| 438 | <span class="lbl">main</span> | 448 | <span class="lbl">main</span> | ||
| 439 | <span class="k">branch</span> | 449 | <span class="k">branch</span> | ||
| 440 | </a> | 450 | </a> | ||
| 441 | </li> | 451 | </li> | ||
| 442 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v0.1.0"> | 452 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v0.1.0"> |
| 443 | <a href="/demo/tree/v0.1.0" tabindex="-1"> | 453 | <a href="/demo/tree/v0.1.0" tabindex="-1"> | ||
| 444 | <span class="lbl">v0.1.0</span> | 454 | <span class="lbl">v0.1.0</span> | ||
| 445 | <span class="k">tag</span> | 455 | <span class="k">tag</span> | ||
| 446 | </a> | 456 | </a> | ||
| 447 | </li> | 457 | </li> | ||
| 448 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v0.2.0"> | 458 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="tag v0.2.0"> |
| 449 | <a href="/demo/tree/v0.2.0" tabindex="-1"> | 459 | <a href="/demo/tree/v0.2.0" tabindex="-1"> | ||
| 450 | <span class="lbl">v0.2.0</span> | 460 | <span class="lbl">v0.2.0</span> | ||
| 451 | <span class="k">tag</span> | 461 | <span class="k">tag</span> | ||
| 452 | </a> | 462 | </a> | ||
| 453 | </li> | 463 | </li> | ||
| 454 | - | <li id="cmd-o11" role="option" aria-selected="false" data-k="repo quiet"> | 464 | + | <li id="cmd-o12" role="option" aria-selected="false" data-k="repo quiet"> |
| 455 | <a href="/quiet" tabindex="-1"> | 465 | <a href="/quiet" tabindex="-1"> | ||
| 456 | <span class="lbl">quiet</span> | 466 | <span class="lbl">quiet</span> | ||
| 457 | <span class="k">repo</span> | 467 | <span class="k">repo</span> | ||
| 458 | </a> | 468 | </a> | ||
| 459 | </li> | 469 | </li> | ||
| 460 | - | <li id="cmd-o12" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 470 | + | <li id="cmd-o13" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 461 | <a href="#" data-fire="keys" tabindex="-1"> | 471 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 462 | <span class="lbl">turn single-key shortcuts off</span> | 472 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 463 | <span class="k">action</span> | 473 | <span class="k">action</span> | ||
M internal/app/testdata/web/summary.html +17 -7
| @@ -84,6 +84,10 @@ | |||
| 84 | 84 | </a> | |
| 85 | 85 | </li> | |
| 86 | 86 | <li> | |
| 87 | + | <a href="/demo/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 88 | + | </a> | |
| 89 | + | </li> | |
| 90 | + | <li> | |
| 87 | 91 | <a href="/demo/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 88 | 92 | </a> | |
| 89 | 93 | </li> | |
| @@ -447,43 +451,49 @@ | |||
| 447 | 451 | <span class="k">section</span> | |
| 448 | 452 | </a> | |
| 449 | 453 | </li> | |
| 450 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 454 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 455 | + | <a href="/demo/releases" tabindex="-1"> | |
| 456 | + | <span class="lbl">releases</span> | |
| 457 | + | <span class="k">section</span> | |
| 458 | + | </a> | |
| 459 | + | </li> | |
| 460 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 451 | 461 | <a href="/demo/actions" tabindex="-1"> | |
| 452 | 462 | <span class="lbl">actions</span> | |
| 453 | 463 | <span class="k">section</span> | |
| 454 | 464 | </a> | |
| 455 | 465 | </li> | |
| 456 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 466 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 457 | 467 | <a href="/demo/tree/feature/x" tabindex="-1"> | |
| 458 | 468 | <span class="lbl">feature/x</span> | |
| 459 | 469 | <span class="k">branch</span> | |
| 460 | 470 | </a> | |
| 461 | 471 | </li> | |
| 462 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 472 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> | |
| 463 | 473 | <a href="/demo/tree/main" tabindex="-1"> | |
| 464 | 474 | <span class="lbl">main</span> | |
| 465 | 475 | <span class="k">branch</span> | |
| 466 | 476 | </a> | |
| 467 | 477 | </li> | |
| 468 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v0.1.0"> | |
| 478 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v0.1.0"> | |
| 469 | 479 | <a href="/demo/tree/v0.1.0" tabindex="-1"> | |
| 470 | 480 | <span class="lbl">v0.1.0</span> | |
| 471 | 481 | <span class="k">tag</span> | |
| 472 | 482 | </a> | |
| 473 | 483 | </li> | |
| 474 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v0.2.0"> | |
| 484 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="tag v0.2.0"> | |
| 475 | 485 | <a href="/demo/tree/v0.2.0" tabindex="-1"> | |
| 476 | 486 | <span class="lbl">v0.2.0</span> | |
| 477 | 487 | <span class="k">tag</span> | |
| 478 | 488 | </a> | |
| 479 | 489 | </li> | |
| 480 | - | <li id="cmd-o11" role="option" aria-selected="false" data-k="repo quiet"> | |
| 490 | + | <li id="cmd-o12" role="option" aria-selected="false" data-k="repo quiet"> | |
| 481 | 491 | <a href="/quiet" tabindex="-1"> | |
| 482 | 492 | <span class="lbl">quiet</span> | |
| 483 | 493 | <span class="k">repo</span> | |
| 484 | 494 | </a> | |
| 485 | 495 | </li> | |
| 486 | - | <li id="cmd-o12" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 496 | + | <li id="cmd-o13" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 487 | 497 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 488 | 498 | <span class="lbl">turn single-key shortcuts off</span> | |
| 489 | 499 | <span class="k">action</span> | |
| @@ -84,6 +84,10 @@ | |||||
| 84 | </a> | 84 | </a> | ||
| 85 | </li> | 85 | </li> | ||
| 86 | <li> | 86 | <li> | ||
| 87 | + | <a href="/demo/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 88 | + | </a> | |||
| 89 | + | </li> | |||
| 90 | + | <li> | |||
| 87 | <a href="/demo/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 91 | <a href="/demo/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 88 | </a> | 92 | </a> | ||
| 89 | </li> | 93 | </li> | ||
| @@ -447,43 +451,49 @@ | |||||
| 447 | <span class="k">section</span> | 451 | <span class="k">section</span> | ||
| 448 | </a> | 452 | </a> | ||
| 449 | </li> | 453 | </li> | ||
| 450 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 454 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 455 | + | <a href="/demo/releases" tabindex="-1"> | |||
| 456 | + | <span class="lbl">releases</span> | |||
| 457 | + | <span class="k">section</span> | |||
| 458 | + | </a> | |||
| 459 | + | </li> | |||
| 460 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 451 | <a href="/demo/actions" tabindex="-1"> | 461 | <a href="/demo/actions" tabindex="-1"> | ||
| 452 | <span class="lbl">actions</span> | 462 | <span class="lbl">actions</span> | ||
| 453 | <span class="k">section</span> | 463 | <span class="k">section</span> | ||
| 454 | </a> | 464 | </a> | ||
| 455 | </li> | 465 | </li> | ||
| 456 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | 466 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> |
| 457 | <a href="/demo/tree/feature/x" tabindex="-1"> | 467 | <a href="/demo/tree/feature/x" tabindex="-1"> | ||
| 458 | <span class="lbl">feature/x</span> | 468 | <span class="lbl">feature/x</span> | ||
| 459 | <span class="k">branch</span> | 469 | <span class="k">branch</span> | ||
| 460 | </a> | 470 | </a> | ||
| 461 | </li> | 471 | </li> | ||
| 462 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | 472 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> |
| 463 | <a href="/demo/tree/main" tabindex="-1"> | 473 | <a href="/demo/tree/main" tabindex="-1"> | ||
| 464 | <span class="lbl">main</span> | 474 | <span class="lbl">main</span> | ||
| 465 | <span class="k">branch</span> | 475 | <span class="k">branch</span> | ||
| 466 | </a> | 476 | </a> | ||
| 467 | </li> | 477 | </li> | ||
| 468 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v0.1.0"> | 478 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v0.1.0"> |
| 469 | <a href="/demo/tree/v0.1.0" tabindex="-1"> | 479 | <a href="/demo/tree/v0.1.0" tabindex="-1"> | ||
| 470 | <span class="lbl">v0.1.0</span> | 480 | <span class="lbl">v0.1.0</span> | ||
| 471 | <span class="k">tag</span> | 481 | <span class="k">tag</span> | ||
| 472 | </a> | 482 | </a> | ||
| 473 | </li> | 483 | </li> | ||
| 474 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v0.2.0"> | 484 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="tag v0.2.0"> |
| 475 | <a href="/demo/tree/v0.2.0" tabindex="-1"> | 485 | <a href="/demo/tree/v0.2.0" tabindex="-1"> | ||
| 476 | <span class="lbl">v0.2.0</span> | 486 | <span class="lbl">v0.2.0</span> | ||
| 477 | <span class="k">tag</span> | 487 | <span class="k">tag</span> | ||
| 478 | </a> | 488 | </a> | ||
| 479 | </li> | 489 | </li> | ||
| 480 | - | <li id="cmd-o11" role="option" aria-selected="false" data-k="repo quiet"> | 490 | + | <li id="cmd-o12" role="option" aria-selected="false" data-k="repo quiet"> |
| 481 | <a href="/quiet" tabindex="-1"> | 491 | <a href="/quiet" tabindex="-1"> | ||
| 482 | <span class="lbl">quiet</span> | 492 | <span class="lbl">quiet</span> | ||
| 483 | <span class="k">repo</span> | 493 | <span class="k">repo</span> | ||
| 484 | </a> | 494 | </a> | ||
| 485 | </li> | 495 | </li> | ||
| 486 | - | <li id="cmd-o12" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 496 | + | <li id="cmd-o13" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 487 | <a href="#" data-fire="keys" tabindex="-1"> | 497 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 488 | <span class="lbl">turn single-key shortcuts off</span> | 498 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 489 | <span class="k">action</span> | 499 | <span class="k">action</span> | ||
M internal/app/testdata/web/tree-nested.html +15 -5
| @@ -96,6 +96,10 @@ | |||
| 96 | 96 | </a> | |
| 97 | 97 | </li> | |
| 98 | 98 | <li> | |
| 99 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 100 | + | </a> | |
| 101 | + | </li> | |
| 102 | + | <li> | |
| 99 | 103 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 100 | 104 | </a> | |
| 101 | 105 | </li> | |
| @@ -373,31 +377,37 @@ | |||
| 373 | 377 | <span class="k">section</span> | |
| 374 | 378 | </a> | |
| 375 | 379 | </li> | |
| 376 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 380 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 381 | + | <a href="/files/releases" tabindex="-1"> | |
| 382 | + | <span class="lbl">releases</span> | |
| 383 | + | <span class="k">section</span> | |
| 384 | + | </a> | |
| 385 | + | </li> | |
| 386 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 377 | 387 | <a href="/files/actions" tabindex="-1"> | |
| 378 | 388 | <span class="lbl">actions</span> | |
| 379 | 389 | <span class="k">section</span> | |
| 380 | 390 | </a> | |
| 381 | 391 | </li> | |
| 382 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 392 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 383 | 393 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 384 | 394 | <span class="lbl">feature/x</span> | |
| 385 | 395 | <span class="k">branch</span> | |
| 386 | 396 | </a> | |
| 387 | 397 | </li> | |
| 388 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 398 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> | |
| 389 | 399 | <a href="/files/tree/main" tabindex="-1"> | |
| 390 | 400 | <span class="lbl">main</span> | |
| 391 | 401 | <span class="k">branch</span> | |
| 392 | 402 | </a> | |
| 393 | 403 | </li> | |
| 394 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 404 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 395 | 405 | <a href="/files/tree/v1.0" tabindex="-1"> | |
| 396 | 406 | <span class="lbl">v1.0</span> | |
| 397 | 407 | <span class="k">tag</span> | |
| 398 | 408 | </a> | |
| 399 | 409 | </li> | |
| 400 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 410 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 401 | 411 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 402 | 412 | <span class="lbl">turn single-key shortcuts off</span> | |
| 403 | 413 | <span class="k">action</span> | |
| @@ -96,6 +96,10 @@ | |||||
| 96 | </a> | 96 | </a> | ||
| 97 | </li> | 97 | </li> | ||
| 98 | <li> | 98 | <li> | ||
| 99 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 100 | + | </a> | |||
| 101 | + | </li> | |||
| 102 | + | <li> | |||
| 99 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 103 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 100 | </a> | 104 | </a> | ||
| 101 | </li> | 105 | </li> | ||
| @@ -373,31 +377,37 @@ | |||||
| 373 | <span class="k">section</span> | 377 | <span class="k">section</span> | ||
| 374 | </a> | 378 | </a> | ||
| 375 | </li> | 379 | </li> | ||
| 376 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 380 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 381 | + | <a href="/files/releases" tabindex="-1"> | |||
| 382 | + | <span class="lbl">releases</span> | |||
| 383 | + | <span class="k">section</span> | |||
| 384 | + | </a> | |||
| 385 | + | </li> | |||
| 386 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 377 | <a href="/files/actions" tabindex="-1"> | 387 | <a href="/files/actions" tabindex="-1"> | ||
| 378 | <span class="lbl">actions</span> | 388 | <span class="lbl">actions</span> | ||
| 379 | <span class="k">section</span> | 389 | <span class="k">section</span> | ||
| 380 | </a> | 390 | </a> | ||
| 381 | </li> | 391 | </li> | ||
| 382 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | 392 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> |
| 383 | <a href="/files/tree/feature/x" tabindex="-1"> | 393 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 384 | <span class="lbl">feature/x</span> | 394 | <span class="lbl">feature/x</span> | ||
| 385 | <span class="k">branch</span> | 395 | <span class="k">branch</span> | ||
| 386 | </a> | 396 | </a> | ||
| 387 | </li> | 397 | </li> | ||
| 388 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | 398 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> |
| 389 | <a href="/files/tree/main" tabindex="-1"> | 399 | <a href="/files/tree/main" tabindex="-1"> | ||
| 390 | <span class="lbl">main</span> | 400 | <span class="lbl">main</span> | ||
| 391 | <span class="k">branch</span> | 401 | <span class="k">branch</span> | ||
| 392 | </a> | 402 | </a> | ||
| 393 | </li> | 403 | </li> | ||
| 394 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | 404 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> |
| 395 | <a href="/files/tree/v1.0" tabindex="-1"> | 405 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 396 | <span class="lbl">v1.0</span> | 406 | <span class="lbl">v1.0</span> | ||
| 397 | <span class="k">tag</span> | 407 | <span class="k">tag</span> | ||
| 398 | </a> | 408 | </a> | ||
| 399 | </li> | 409 | </li> | ||
| 400 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 410 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 401 | <a href="#" data-fire="keys" tabindex="-1"> | 411 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 402 | <span class="lbl">turn single-key shortcuts off</span> | 412 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 403 | <span class="k">action</span> | 413 | <span class="k">action</span> | ||
M internal/app/testdata/web/tree-root.html +15 -5
| @@ -90,6 +90,10 @@ | |||
| 90 | 90 | </a> | |
| 91 | 91 | </li> | |
| 92 | 92 | <li> | |
| 93 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 94 | + | </a> | |
| 95 | + | </li> | |
| 96 | + | <li> | |
| 93 | 97 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 94 | 98 | </a> | |
| 95 | 99 | </li> | |
| @@ -594,31 +598,37 @@ | |||
| 594 | 598 | <span class="k">section</span> | |
| 595 | 599 | </a> | |
| 596 | 600 | </li> | |
| 597 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 601 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 602 | + | <a href="/files/releases" tabindex="-1"> | |
| 603 | + | <span class="lbl">releases</span> | |
| 604 | + | <span class="k">section</span> | |
| 605 | + | </a> | |
| 606 | + | </li> | |
| 607 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 598 | 608 | <a href="/files/actions" tabindex="-1"> | |
| 599 | 609 | <span class="lbl">actions</span> | |
| 600 | 610 | <span class="k">section</span> | |
| 601 | 611 | </a> | |
| 602 | 612 | </li> | |
| 603 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 613 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 604 | 614 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 605 | 615 | <span class="lbl">feature/x</span> | |
| 606 | 616 | <span class="k">branch</span> | |
| 607 | 617 | </a> | |
| 608 | 618 | </li> | |
| 609 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 619 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> | |
| 610 | 620 | <a href="/files/tree/main" tabindex="-1"> | |
| 611 | 621 | <span class="lbl">main</span> | |
| 612 | 622 | <span class="k">branch</span> | |
| 613 | 623 | </a> | |
| 614 | 624 | </li> | |
| 615 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 625 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 616 | 626 | <a href="/files/tree/v1.0" tabindex="-1"> | |
| 617 | 627 | <span class="lbl">v1.0</span> | |
| 618 | 628 | <span class="k">tag</span> | |
| 619 | 629 | </a> | |
| 620 | 630 | </li> | |
| 621 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 631 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 622 | 632 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 623 | 633 | <span class="lbl">turn single-key shortcuts off</span> | |
| 624 | 634 | <span class="k">action</span> | |
| @@ -90,6 +90,10 @@ | |||||
| 90 | </a> | 90 | </a> | ||
| 91 | </li> | 91 | </li> | ||
| 92 | <li> | 92 | <li> | ||
| 93 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 94 | + | </a> | |||
| 95 | + | </li> | |||
| 96 | + | <li> | |||
| 93 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 97 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 94 | </a> | 98 | </a> | ||
| 95 | </li> | 99 | </li> | ||
| @@ -594,31 +598,37 @@ | |||||
| 594 | <span class="k">section</span> | 598 | <span class="k">section</span> | ||
| 595 | </a> | 599 | </a> | ||
| 596 | </li> | 600 | </li> | ||
| 597 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 601 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 602 | + | <a href="/files/releases" tabindex="-1"> | |||
| 603 | + | <span class="lbl">releases</span> | |||
| 604 | + | <span class="k">section</span> | |||
| 605 | + | </a> | |||
| 606 | + | </li> | |||
| 607 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 598 | <a href="/files/actions" tabindex="-1"> | 608 | <a href="/files/actions" tabindex="-1"> | ||
| 599 | <span class="lbl">actions</span> | 609 | <span class="lbl">actions</span> | ||
| 600 | <span class="k">section</span> | 610 | <span class="k">section</span> | ||
| 601 | </a> | 611 | </a> | ||
| 602 | </li> | 612 | </li> | ||
| 603 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | 613 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> |
| 604 | <a href="/files/tree/feature/x" tabindex="-1"> | 614 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 605 | <span class="lbl">feature/x</span> | 615 | <span class="lbl">feature/x</span> | ||
| 606 | <span class="k">branch</span> | 616 | <span class="k">branch</span> | ||
| 607 | </a> | 617 | </a> | ||
| 608 | </li> | 618 | </li> | ||
| 609 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | 619 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> |
| 610 | <a href="/files/tree/main" tabindex="-1"> | 620 | <a href="/files/tree/main" tabindex="-1"> | ||
| 611 | <span class="lbl">main</span> | 621 | <span class="lbl">main</span> | ||
| 612 | <span class="k">branch</span> | 622 | <span class="k">branch</span> | ||
| 613 | </a> | 623 | </a> | ||
| 614 | </li> | 624 | </li> | ||
| 615 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | 625 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> |
| 616 | <a href="/files/tree/v1.0" tabindex="-1"> | 626 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 617 | <span class="lbl">v1.0</span> | 627 | <span class="lbl">v1.0</span> | ||
| 618 | <span class="k">tag</span> | 628 | <span class="k">tag</span> | ||
| 619 | </a> | 629 | </a> | ||
| 620 | </li> | 630 | </li> | ||
| 621 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 631 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 622 | <a href="#" data-fire="keys" tabindex="-1"> | 632 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 623 | <span class="lbl">turn single-key shortcuts off</span> | 633 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 624 | <span class="k">action</span> | 634 | <span class="k">action</span> | ||
M internal/app/testdata/web/tree-slash-ref.html +15 -5
| @@ -93,6 +93,10 @@ | |||
| 93 | 93 | </a> | |
| 94 | 94 | </li> | |
| 95 | 95 | <li> | |
| 96 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 97 | + | </a> | |
| 98 | + | </li> | |
| 99 | + | <li> | |
| 96 | 100 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 97 | 101 | </a> | |
| 98 | 102 | </li> | |
| @@ -370,31 +374,37 @@ | |||
| 370 | 374 | <span class="k">section</span> | |
| 371 | 375 | </a> | |
| 372 | 376 | </li> | |
| 373 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 377 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 378 | + | <a href="/files/releases" tabindex="-1"> | |
| 379 | + | <span class="lbl">releases</span> | |
| 380 | + | <span class="k">section</span> | |
| 381 | + | </a> | |
| 382 | + | </li> | |
| 383 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 374 | 384 | <a href="/files/actions" tabindex="-1"> | |
| 375 | 385 | <span class="lbl">actions</span> | |
| 376 | 386 | <span class="k">section</span> | |
| 377 | 387 | </a> | |
| 378 | 388 | </li> | |
| 379 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 389 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 380 | 390 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 381 | 391 | <span class="lbl">feature/x</span> | |
| 382 | 392 | <span class="k">branch</span> | |
| 383 | 393 | </a> | |
| 384 | 394 | </li> | |
| 385 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 395 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> | |
| 386 | 396 | <a href="/files/tree/main" tabindex="-1"> | |
| 387 | 397 | <span class="lbl">main</span> | |
| 388 | 398 | <span class="k">branch</span> | |
| 389 | 399 | </a> | |
| 390 | 400 | </li> | |
| 391 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 401 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 392 | 402 | <a href="/files/tree/v1.0" tabindex="-1"> | |
| 393 | 403 | <span class="lbl">v1.0</span> | |
| 394 | 404 | <span class="k">tag</span> | |
| 395 | 405 | </a> | |
| 396 | 406 | </li> | |
| 397 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 407 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 398 | 408 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 399 | 409 | <span class="lbl">turn single-key shortcuts off</span> | |
| 400 | 410 | <span class="k">action</span> | |
| @@ -93,6 +93,10 @@ | |||||
| 93 | </a> | 93 | </a> | ||
| 94 | </li> | 94 | </li> | ||
| 95 | <li> | 95 | <li> | ||
| 96 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 97 | + | </a> | |||
| 98 | + | </li> | |||
| 99 | + | <li> | |||
| 96 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 100 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 97 | </a> | 101 | </a> | ||
| 98 | </li> | 102 | </li> | ||
| @@ -370,31 +374,37 @@ | |||||
| 370 | <span class="k">section</span> | 374 | <span class="k">section</span> | ||
| 371 | </a> | 375 | </a> | ||
| 372 | </li> | 376 | </li> | ||
| 373 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 377 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 378 | + | <a href="/files/releases" tabindex="-1"> | |||
| 379 | + | <span class="lbl">releases</span> | |||
| 380 | + | <span class="k">section</span> | |||
| 381 | + | </a> | |||
| 382 | + | </li> | |||
| 383 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 374 | <a href="/files/actions" tabindex="-1"> | 384 | <a href="/files/actions" tabindex="-1"> | ||
| 375 | <span class="lbl">actions</span> | 385 | <span class="lbl">actions</span> | ||
| 376 | <span class="k">section</span> | 386 | <span class="k">section</span> | ||
| 377 | </a> | 387 | </a> | ||
| 378 | </li> | 388 | </li> | ||
| 379 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | 389 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> |
| 380 | <a href="/files/tree/feature/x" tabindex="-1"> | 390 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 381 | <span class="lbl">feature/x</span> | 391 | <span class="lbl">feature/x</span> | ||
| 382 | <span class="k">branch</span> | 392 | <span class="k">branch</span> | ||
| 383 | </a> | 393 | </a> | ||
| 384 | </li> | 394 | </li> | ||
| 385 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | 395 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> |
| 386 | <a href="/files/tree/main" tabindex="-1"> | 396 | <a href="/files/tree/main" tabindex="-1"> | ||
| 387 | <span class="lbl">main</span> | 397 | <span class="lbl">main</span> | ||
| 388 | <span class="k">branch</span> | 398 | <span class="k">branch</span> | ||
| 389 | </a> | 399 | </a> | ||
| 390 | </li> | 400 | </li> | ||
| 391 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | 401 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> |
| 392 | <a href="/files/tree/v1.0" tabindex="-1"> | 402 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 393 | <span class="lbl">v1.0</span> | 403 | <span class="lbl">v1.0</span> | ||
| 394 | <span class="k">tag</span> | 404 | <span class="k">tag</span> | ||
| 395 | </a> | 405 | </a> | ||
| 396 | </li> | 406 | </li> | ||
| 397 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 407 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 398 | <a href="#" data-fire="keys" tabindex="-1"> | 408 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 399 | <span class="lbl">turn single-key shortcuts off</span> | 409 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 400 | <span class="k">action</span> | 410 | <span class="k">action</span> | ||
M internal/app/testdata/web/tree-subdir.html +15 -5
| @@ -93,6 +93,10 @@ | |||
| 93 | 93 | </a> | |
| 94 | 94 | </li> | |
| 95 | 95 | <li> | |
| 96 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |
| 97 | + | </a> | |
| 98 | + | </li> | |
| 99 | + | <li> | |
| 96 | 100 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | |
| 97 | 101 | </a> | |
| 98 | 102 | </li> | |
| @@ -386,31 +390,37 @@ | |||
| 386 | 390 | <span class="k">section</span> | |
| 387 | 391 | </a> | |
| 388 | 392 | </li> | |
| 389 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | |
| 393 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> | |
| 394 | + | <a href="/files/releases" tabindex="-1"> | |
| 395 | + | <span class="lbl">releases</span> | |
| 396 | + | <span class="k">section</span> | |
| 397 | + | </a> | |
| 398 | + | </li> | |
| 399 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |
| 390 | 400 | <a href="/files/actions" tabindex="-1"> | |
| 391 | 401 | <span class="lbl">actions</span> | |
| 392 | 402 | <span class="k">section</span> | |
| 393 | 403 | </a> | |
| 394 | 404 | </li> | |
| 395 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 405 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> | |
| 396 | 406 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 397 | 407 | <span class="lbl">feature/x</span> | |
| 398 | 408 | <span class="k">branch</span> | |
| 399 | 409 | </a> | |
| 400 | 410 | </li> | |
| 401 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | |
| 411 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> | |
| 402 | 412 | <a href="/files/tree/main" tabindex="-1"> | |
| 403 | 413 | <span class="lbl">main</span> | |
| 404 | 414 | <span class="k">branch</span> | |
| 405 | 415 | </a> | |
| 406 | 416 | </li> | |
| 407 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 417 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> | |
| 408 | 418 | <a href="/files/tree/v1.0" tabindex="-1"> | |
| 409 | 419 | <span class="lbl">v1.0</span> | |
| 410 | 420 | <span class="k">tag</span> | |
| 411 | 421 | </a> | |
| 412 | 422 | </li> | |
| 413 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 423 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | |
| 414 | 424 | <a href="#" data-fire="keys" tabindex="-1"> | |
| 415 | 425 | <span class="lbl">turn single-key shortcuts off</span> | |
| 416 | 426 | <span class="k">action</span> | |
| @@ -93,6 +93,10 @@ | |||||
| 93 | </a> | 93 | </a> | ||
| 94 | </li> | 94 | </li> | ||
| 95 | <li> | 95 | <li> | ||
| 96 | + | <a href="/files/releases" data-key="l" aria-keyshortcuts="l">Releases <kbd aria-hidden="true">l</kbd> | |||
| 97 | + | </a> | |||
| 98 | + | </li> | |||
| 99 | + | <li> | |||
| 96 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | 100 | <a href="/files/actions" data-key="a" aria-keyshortcuts="a">Actions <kbd aria-hidden="true">a</kbd> | ||
| 97 | </a> | 101 | </a> | ||
| 98 | </li> | 102 | </li> | ||
| @@ -386,31 +390,37 @@ | |||||
| 386 | <span class="k">section</span> | 390 | <span class="k">section</span> | ||
| 387 | </a> | 391 | </a> | ||
| 388 | </li> | 392 | </li> | ||
| 389 | - | <li id="cmd-o6" role="option" aria-selected="false" data-k="section actions"> | 393 | + | <li id="cmd-o6" role="option" aria-selected="false" data-k="section releases"> |
| 394 | + | <a href="/files/releases" tabindex="-1"> | |||
| 395 | + | <span class="lbl">releases</span> | |||
| 396 | + | <span class="k">section</span> | |||
| 397 | + | </a> | |||
| 398 | + | </li> | |||
| 399 | + | <li id="cmd-o7" role="option" aria-selected="false" data-k="section actions"> | |||
| 390 | <a href="/files/actions" tabindex="-1"> | 400 | <a href="/files/actions" tabindex="-1"> | ||
| 391 | <span class="lbl">actions</span> | 401 | <span class="lbl">actions</span> | ||
| 392 | <span class="k">section</span> | 402 | <span class="k">section</span> | ||
| 393 | </a> | 403 | </a> | ||
| 394 | </li> | 404 | </li> | ||
| 395 | - | <li id="cmd-o7" role="option" aria-selected="false" data-k="branch feature/x"> | 405 | + | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch feature/x"> |
| 396 | <a href="/files/tree/feature/x" tabindex="-1"> | 406 | <a href="/files/tree/feature/x" tabindex="-1"> | ||
| 397 | <span class="lbl">feature/x</span> | 407 | <span class="lbl">feature/x</span> | ||
| 398 | <span class="k">branch</span> | 408 | <span class="k">branch</span> | ||
| 399 | </a> | 409 | </a> | ||
| 400 | </li> | 410 | </li> | ||
| 401 | - | <li id="cmd-o8" role="option" aria-selected="false" data-k="branch main"> | 411 | + | <li id="cmd-o9" role="option" aria-selected="false" data-k="branch main"> |
| 402 | <a href="/files/tree/main" tabindex="-1"> | 412 | <a href="/files/tree/main" tabindex="-1"> | ||
| 403 | <span class="lbl">main</span> | 413 | <span class="lbl">main</span> | ||
| 404 | <span class="k">branch</span> | 414 | <span class="k">branch</span> | ||
| 405 | </a> | 415 | </a> | ||
| 406 | </li> | 416 | </li> | ||
| 407 | - | <li id="cmd-o9" role="option" aria-selected="false" data-k="tag v1.0"> | 417 | + | <li id="cmd-o10" role="option" aria-selected="false" data-k="tag v1.0"> |
| 408 | <a href="/files/tree/v1.0" tabindex="-1"> | 418 | <a href="/files/tree/v1.0" tabindex="-1"> | ||
| 409 | <span class="lbl">v1.0</span> | 419 | <span class="lbl">v1.0</span> | ||
| 410 | <span class="k">tag</span> | 420 | <span class="k">tag</span> | ||
| 411 | </a> | 421 | </a> | ||
| 412 | </li> | 422 | </li> | ||
| 413 | - | <li id="cmd-o10" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> | 423 | + | <li id="cmd-o11" role="option" aria-selected="false" data-k="action turn single-key shortcuts on off keyboard"> |
| 414 | <a href="#" data-fire="keys" tabindex="-1"> | 424 | <a href="#" data-fire="keys" tabindex="-1"> | ||
| 415 | <span class="lbl">turn single-key shortcuts off</span> | 425 | <span class="lbl">turn single-key shortcuts off</span> | ||
| 416 | <span class="k">action</span> | 426 | <span class="k">action</span> | ||
M internal/app/web.go +1 -0
| @@ -57,6 +57,7 @@ func newWeb(cfg config.Config, f *forge, limiter *clientlimit.Limiter) (http.Han | |||
| 57 | 57 | SigningKey: signing, | |
| 58 | 58 | Runs: f.CI, | |
| 59 | 59 | Artifacts: artifacts, | |
| 60 | + | Releases: f.Releases, | |
| 60 | 61 | Events: f.EventLog, | |
| 61 | 62 | Mirrors: f.Mirrors, | |
| 62 | 63 | Owner: cfg.Owner, | |
| @@ -57,6 +57,7 @@ func newWeb(cfg config.Config, f *forge, limiter *clientlimit.Limiter) (http.Han | |||||
| 57 | SigningKey: signing, | 57 | SigningKey: signing, | ||
| 58 | Runs: f.CI, | 58 | Runs: f.CI, | ||
| 59 | Artifacts: artifacts, | 59 | Artifacts: artifacts, | ||
| 60 | + | Releases: f.Releases, | |||
| 60 | Events: f.EventLog, | 61 | Events: f.EventLog, | ||
| 61 | Mirrors: f.Mirrors, | 62 | Mirrors: f.Mirrors, | ||
| 62 | Owner: cfg.Owner, | 63 | Owner: cfg.Owner, | ||
M internal/app/web_release_test.go +3 -12
| @@ -6,7 +6,6 @@ import ( | |||
| 6 | 6 | "context" | |
| 7 | 7 | "io" | |
| 8 | 8 | "net/http" | |
| 9 | - | "net/http/httptest" | |
| 10 | 9 | "strconv" | |
| 11 | 10 | "strings" | |
| 12 | 11 | "testing" | |
| @@ -14,20 +13,15 @@ import ( | |||
| 14 | 13 | "github.com/stretchr/testify/assert" | |
| 15 | 14 | "github.com/stretchr/testify/require" | |
| 16 | 15 | ||
| 17 | - | "bnema.dev/ermite/internal/adapters/blobdisk" | |
| 18 | - | "bnema.dev/ermite/internal/adapters/web" | |
| 19 | 16 | "bnema.dev/ermite/internal/domain" | |
| 20 | 17 | "bnema.dev/ermite/internal/ports" | |
| 21 | - | "bnema.dev/ermite/internal/usecase" | |
| 22 | 18 | ) | |
| 23 | 19 | ||
| 24 | - | // P8 owns production wiring. Until then, this test wires the real release | |
| 25 | - | // use case, forge metadata/Git, disk blobs and web in a test-local server. | |
| 20 | + | // Releases and web use the production forge wiring. | |
| 26 | 21 | func TestWebRelease(t *testing.T) { | |
| 27 | 22 | h := newHarness(t, harnessOpts{}) | |
| 28 | 23 | h.webRepo("demo") | |
| 29 | - | blobs := blobdisk.New(t.TempDir()) | |
| 30 | - | releases := usecase.NewReleases(h.forge.store.Releases(), blobs, h.forge.store, h.forge.git, h.forge.git, h.forge.Pushes, h.forge.Bus, systemClock{}, ports.ReleaseLimits{}) | |
| 24 | + | blobs, releases := h.forge.Blobs, h.forge.Releases | |
| 31 | 25 | repo := h.name("demo") | |
| 32 | 26 | rel, err := releases.CreateRelease(h.ctx, domain.Root(), repo, ports.CreateReleaseParams{Tag: "v0.2.0", Title: "Demo release", Notes: "## Install\n\n**Download** the binary. <script>alert(1)</script>"}) | |
| 33 | 27 | require.NoError(t, err) | |
| @@ -38,10 +32,7 @@ func TestWebRelease(t *testing.T) { | |||
| 38 | 32 | require.NoError(t, err) | |
| 39 | 33 | draftAsset, err := releases.UploadAsset(h.ctx, domain.Root(), repo, ports.UploadAssetParams{ReleaseID: draft.ID, Name: "hidden.bin", Size: 1}, strings.NewReader("x")) | |
| 40 | 34 | require.NoError(t, err) | |
| 41 | - | srv, err := web.New(web.Config{Repos: h.forge.Repos, Browse: h.forge.Browse, Releases: releases, PublicURL: "http://example.test"}) | |
| 42 | - | require.NoError(t, err) | |
| 43 | - | ts := httptest.NewServer(srv) | |
| 44 | - | t.Cleanup(ts.Close) | |
| 35 | + | ts := h.srv | |
| 45 | 36 | get := func(method, path, byteRange string) (int, http.Header, string) { | |
| 46 | 37 | t.Helper() | |
| 47 | 38 | req, err := http.NewRequestWithContext(t.Context(), method, ts.URL+path, nil) | |
| @@ -6,7 +6,6 @@ import ( | |||||
| 6 | "context" | 6 | "context" | ||
| 7 | "io" | 7 | "io" | ||
| 8 | "net/http" | 8 | "net/http" | ||
| 9 | - | "net/http/httptest" | |||
| 10 | "strconv" | 9 | "strconv" | ||
| 11 | "strings" | 10 | "strings" | ||
| 12 | "testing" | 11 | "testing" | ||
| @@ -14,20 +13,15 @@ import ( | |||||
| 14 | "github.com/stretchr/testify/assert" | 13 | "github.com/stretchr/testify/assert" | ||
| 15 | "github.com/stretchr/testify/require" | 14 | "github.com/stretchr/testify/require" | ||
| 16 | 15 | ||||
| 17 | - | "bnema.dev/ermite/internal/adapters/blobdisk" | |||
| 18 | - | "bnema.dev/ermite/internal/adapters/web" | |||
| 19 | "bnema.dev/ermite/internal/domain" | 16 | "bnema.dev/ermite/internal/domain" | ||
| 20 | "bnema.dev/ermite/internal/ports" | 17 | "bnema.dev/ermite/internal/ports" | ||
| 21 | - | "bnema.dev/ermite/internal/usecase" | |||
| 22 | ) | 18 | ) | ||
| 23 | 19 | ||||
| 24 | - | // P8 owns production wiring. Until then, this test wires the real release | 20 | + | // Releases and web use the production forge wiring. |
| 25 | - | // use case, forge metadata/Git, disk blobs and web in a test-local server. | |||
| 26 | func TestWebRelease(t *testing.T) { | 21 | func TestWebRelease(t *testing.T) { | ||
| 27 | h := newHarness(t, harnessOpts{}) | 22 | h := newHarness(t, harnessOpts{}) | ||
| 28 | h.webRepo("demo") | 23 | h.webRepo("demo") | ||
| 29 | - | blobs := blobdisk.New(t.TempDir()) | 24 | + | blobs, releases := h.forge.Blobs, h.forge.Releases |
| 30 | - | releases := usecase.NewReleases(h.forge.store.Releases(), blobs, h.forge.store, h.forge.git, h.forge.git, h.forge.Pushes, h.forge.Bus, systemClock{}, ports.ReleaseLimits{}) | |||
| 31 | repo := h.name("demo") | 25 | repo := h.name("demo") | ||
| 32 | rel, err := releases.CreateRelease(h.ctx, domain.Root(), repo, ports.CreateReleaseParams{Tag: "v0.2.0", Title: "Demo release", Notes: "## Install\n\n**Download** the binary. <script>alert(1)</script>"}) | 26 | rel, err := releases.CreateRelease(h.ctx, domain.Root(), repo, ports.CreateReleaseParams{Tag: "v0.2.0", Title: "Demo release", Notes: "## Install\n\n**Download** the binary. <script>alert(1)</script>"}) | ||
| 33 | require.NoError(t, err) | 27 | require.NoError(t, err) | ||
| @@ -38,10 +32,7 @@ func TestWebRelease(t *testing.T) { | |||||
| 38 | require.NoError(t, err) | 32 | require.NoError(t, err) | ||
| 39 | draftAsset, err := releases.UploadAsset(h.ctx, domain.Root(), repo, ports.UploadAssetParams{ReleaseID: draft.ID, Name: "hidden.bin", Size: 1}, strings.NewReader("x")) | 33 | draftAsset, err := releases.UploadAsset(h.ctx, domain.Root(), repo, ports.UploadAssetParams{ReleaseID: draft.ID, Name: "hidden.bin", Size: 1}, strings.NewReader("x")) | ||
| 40 | require.NoError(t, err) | 34 | require.NoError(t, err) | ||
| 41 | - | srv, err := web.New(web.Config{Repos: h.forge.Repos, Browse: h.forge.Browse, Releases: releases, PublicURL: "http://example.test"}) | 35 | + | ts := h.srv |
| 42 | - | require.NoError(t, err) | |||
| 43 | - | ts := httptest.NewServer(srv) | |||
| 44 | - | t.Cleanup(ts.Close) | |||
| 45 | get := func(method, path, byteRange string) (int, http.Header, string) { | 36 | get := func(method, path, byteRange string) (int, http.Header, string) { | ||
| 46 | t.Helper() | 37 | t.Helper() | ||
| 47 | req, err := http.NewRequestWithContext(t.Context(), method, ts.URL+path, nil) | 38 | req, err := http.NewRequestWithContext(t.Context(), method, ts.URL+path, nil) | ||
M internal/config/config.go +117 -0
| @@ -12,6 +12,7 @@ import ( | |||
| 12 | 12 | "net" | |
| 13 | 13 | "net/netip" | |
| 14 | 14 | "net/url" | |
| 15 | + | "os" | |
| 15 | 16 | "path/filepath" | |
| 16 | 17 | "strconv" | |
| 17 | 18 | "strings" | |
| @@ -56,6 +57,16 @@ const ( | |||
| 56 | 57 | EnvCIArtifactRetention = "ERMITE_CI_ARTIFACT_RETENTION" | |
| 57 | 58 | EnvCIArtifactQuota = "ERMITE_CI_ARTIFACT_QUOTA" | |
| 58 | 59 | EnvCIArtifactMax = "ERMITE_CI_ARTIFACT_MAX" | |
| 60 | + | EnvS3Bucket = "ERMITE_S3_BUCKET" | |
| 61 | + | EnvS3Endpoint = "ERMITE_S3_ENDPOINT" | |
| 62 | + | EnvS3Region = "ERMITE_S3_REGION" | |
| 63 | + | EnvS3AccessKeyID = "ERMITE_S3_ACCESS_KEY_ID" | |
| 64 | + | EnvS3SecretAccessKey = "ERMITE_S3_SECRET_ACCESS_KEY" | |
| 65 | + | EnvS3ForcePathStyle = "ERMITE_S3_FORCE_PATH_STYLE" | |
| 66 | + | EnvS3Prefix = "ERMITE_S3_PREFIX" | |
| 67 | + | EnvReleaseAssetMax = "ERMITE_RELEASE_ASSET_MAX" | |
| 68 | + | EnvReleaseAssetsMax = "ERMITE_RELEASE_ASSETS_MAX" | |
| 69 | + | EnvReleaseQuota = "ERMITE_RELEASE_QUOTA" | |
| 59 | 70 | ) | |
| 60 | 71 | ||
| 61 | 72 | // SecretsKeyLen is the length of the secrets key in bytes (AES-256). | |
| @@ -140,6 +151,12 @@ type Config struct { | |||
| 140 | 151 | CIArtifactRetention time.Duration | |
| 141 | 152 | CIArtifactQuota int64 | |
| 142 | 153 | CIArtifactMax int64 | |
| 154 | + | // S3Bucket selects S3; empty selects local disk. Credentials are secrets. | |
| 155 | + | S3Bucket, S3Endpoint, S3Region, S3AccessKeyID, S3SecretAccessKey, S3Prefix string | |
| 156 | + | S3ForcePathStyle bool | |
| 157 | + | // Zero release limits leave defaults to the release use case. | |
| 158 | + | ReleaseAssetMax, ReleaseQuota int64 | |
| 159 | + | ReleaseAssetsMax int | |
| 143 | 160 | } | |
| 144 | 161 | ||
| 145 | 162 | // Error lists every configuration problem found, in variable order. | |
| @@ -167,6 +184,7 @@ func Load(get func(string) string) (Config, error) { | |||
| 167 | 184 | bad := func(key, format string, args ...any) { | |
| 168 | 185 | problems = append(problems, key+": "+fmt.Sprintf(format, args...)) | |
| 169 | 186 | } | |
| 187 | + | get = resolveFiles(get, &problems) | |
| 170 | 188 | var c Config | |
| 171 | 189 | ||
| 172 | 190 | c.Addr = get(EnvAddr) | |
| @@ -342,12 +360,111 @@ func Load(get func(string) string) (Config, error) { | |||
| 342 | 360 | } | |
| 343 | 361 | } | |
| 344 | 362 | ||
| 363 | + | loadStorage(get, &c, &problems) | |
| 345 | 364 | if len(problems) > 0 { | |
| 346 | 365 | return Config{}, &Error{Problems: problems} | |
| 347 | 366 | } | |
| 348 | 367 | return c, nil | |
| 349 | 368 | } | |
| 350 | 369 | ||
| 370 | + | // resolveFiles resolves only the four supported secrets, once at startup. | |
| 371 | + | // Neither file paths, file errors nor secret contents enter diagnostics. | |
| 372 | + | func resolveFiles(get func(string) string, problems *[]string) func(string) string { | |
| 373 | + | values := make(map[string]string) | |
| 374 | + | for _, key := range []string{EnvAdminToken, EnvSecretsKey, EnvS3AccessKeyID, EnvS3SecretAccessKey} { | |
| 375 | + | value, file := get(key), get(key+"_FILE") | |
| 376 | + | if value != "" && file != "" { | |
| 377 | + | *problems = append(*problems, key+": cannot set both variable and _FILE") | |
| 378 | + | } else if file != "" { | |
| 379 | + | data, err := os.ReadFile(file) | |
| 380 | + | if err != nil { | |
| 381 | + | *problems = append(*problems, key+"_FILE: cannot read secret file") | |
| 382 | + | } else { | |
| 383 | + | value = strings.TrimRight(string(data), "\r\n") | |
| 384 | + | if value == "" { | |
| 385 | + | *problems = append(*problems, key+"_FILE: secret file is empty") | |
| 386 | + | } | |
| 387 | + | } | |
| 388 | + | } | |
| 389 | + | values[key] = value | |
| 390 | + | } | |
| 391 | + | return func(key string) string { | |
| 392 | + | if value, ok := values[key]; ok { | |
| 393 | + | return value | |
| 394 | + | } | |
| 395 | + | return get(key) | |
| 396 | + | } | |
| 397 | + | } | |
| 398 | + | ||
| 399 | + | func loadStorage(get func(string) string, c *Config, problems *[]string) { | |
| 400 | + | bad := func(key, message string) { *problems = append(*problems, key+": "+message) } | |
| 401 | + | c.S3Bucket = get(EnvS3Bucket) | |
| 402 | + | c.S3Endpoint = get(EnvS3Endpoint) | |
| 403 | + | c.S3Region = get(EnvS3Region) | |
| 404 | + | c.S3AccessKeyID = get(EnvS3AccessKeyID) | |
| 405 | + | c.S3SecretAccessKey = get(EnvS3SecretAccessKey) | |
| 406 | + | c.S3Prefix = strings.Trim(get(EnvS3Prefix), "/") | |
| 407 | + | if c.S3Prefix != "" { | |
| 408 | + | valid := len(c.S3Prefix) <= 512 | |
| 409 | + | for _, part := range strings.Split(c.S3Prefix, "/") { | |
| 410 | + | if part == "" || part == "." || part == ".." { | |
| 411 | + | valid = false | |
| 412 | + | } | |
| 413 | + | } | |
| 414 | + | for _, r := range c.S3Prefix { | |
| 415 | + | if r < 0x21 || r > 0x7e || r == '\\' { | |
| 416 | + | valid = false | |
| 417 | + | } | |
| 418 | + | } | |
| 419 | + | if !valid { | |
| 420 | + | bad(EnvS3Prefix, "must be an ASCII key prefix without empty, dot or dot-dot segments") | |
| 421 | + | } | |
| 422 | + | c.S3Prefix += "/" | |
| 423 | + | } | |
| 424 | + | if c.S3Endpoint != "" { | |
| 425 | + | if endpoint, err := parseURL(c.S3Endpoint, false); err != nil { | |
| 426 | + | bad(EnvS3Endpoint, "must be an http(s) URL without credentials, path, query or fragment") | |
| 427 | + | } else { | |
| 428 | + | c.S3Endpoint = endpoint | |
| 429 | + | } | |
| 430 | + | } | |
| 431 | + | switch get(EnvS3ForcePathStyle) { | |
| 432 | + | case "", "0", "false": | |
| 433 | + | case "1", "true": | |
| 434 | + | c.S3ForcePathStyle = true | |
| 435 | + | default: | |
| 436 | + | bad(EnvS3ForcePathStyle, "must be 0, 1, false or true") | |
| 437 | + | } | |
| 438 | + | if c.S3Bucket != "" { | |
| 439 | + | for _, v := range []struct{ key, value string }{{EnvS3Region, c.S3Region}, {EnvS3AccessKeyID, c.S3AccessKeyID}, {EnvS3SecretAccessKey, c.S3SecretAccessKey}} { | |
| 440 | + | if strings.TrimSpace(v.value) == "" { | |
| 441 | + | bad(v.key, "required when ERMITE_S3_BUCKET is set") | |
| 442 | + | } | |
| 443 | + | } | |
| 444 | + | } | |
| 445 | + | for _, v := range []struct { | |
| 446 | + | key string | |
| 447 | + | dst *int64 | |
| 448 | + | }{{EnvReleaseAssetMax, &c.ReleaseAssetMax}, {EnvReleaseQuota, &c.ReleaseQuota}} { | |
| 449 | + | if raw := strings.TrimSpace(get(v.key)); raw != "" { | |
| 450 | + | n, err := parseSize(raw) | |
| 451 | + | if err != nil { | |
| 452 | + | bad(v.key, err.Error()) | |
| 453 | + | } else { | |
| 454 | + | *v.dst = n | |
| 455 | + | } | |
| 456 | + | } | |
| 457 | + | } | |
| 458 | + | if raw := get(EnvReleaseAssetsMax); raw != "" { | |
| 459 | + | n, err := strconv.Atoi(raw) | |
| 460 | + | if err != nil || n < 1 { | |
| 461 | + | bad(EnvReleaseAssetsMax, "must be a positive integer") | |
| 462 | + | } else { | |
| 463 | + | c.ReleaseAssetsMax = n | |
| 464 | + | } | |
| 465 | + | } | |
| 466 | + | } | |
| 467 | + | ||
| 351 | 468 | // sizeUnits are the suffixes of parseSize, longest first. | |
| 352 | 469 | var sizeUnits = []struct { | |
| 353 | 470 | suffix string | |
| @@ -12,6 +12,7 @@ import ( | |||||
| 12 | "net" | 12 | "net" | ||
| 13 | "net/netip" | 13 | "net/netip" | ||
| 14 | "net/url" | 14 | "net/url" | ||
| 15 | + | "os" | |||
| 15 | "path/filepath" | 16 | "path/filepath" | ||
| 16 | "strconv" | 17 | "strconv" | ||
| 17 | "strings" | 18 | "strings" | ||
| @@ -56,6 +57,16 @@ const ( | |||||
| 56 | EnvCIArtifactRetention = "ERMITE_CI_ARTIFACT_RETENTION" | 57 | EnvCIArtifactRetention = "ERMITE_CI_ARTIFACT_RETENTION" | ||
| 57 | EnvCIArtifactQuota = "ERMITE_CI_ARTIFACT_QUOTA" | 58 | EnvCIArtifactQuota = "ERMITE_CI_ARTIFACT_QUOTA" | ||
| 58 | EnvCIArtifactMax = "ERMITE_CI_ARTIFACT_MAX" | 59 | EnvCIArtifactMax = "ERMITE_CI_ARTIFACT_MAX" | ||
| 60 | + | EnvS3Bucket = "ERMITE_S3_BUCKET" | |||
| 61 | + | EnvS3Endpoint = "ERMITE_S3_ENDPOINT" | |||
| 62 | + | EnvS3Region = "ERMITE_S3_REGION" | |||
| 63 | + | EnvS3AccessKeyID = "ERMITE_S3_ACCESS_KEY_ID" | |||
| 64 | + | EnvS3SecretAccessKey = "ERMITE_S3_SECRET_ACCESS_KEY" | |||
| 65 | + | EnvS3ForcePathStyle = "ERMITE_S3_FORCE_PATH_STYLE" | |||
| 66 | + | EnvS3Prefix = "ERMITE_S3_PREFIX" | |||
| 67 | + | EnvReleaseAssetMax = "ERMITE_RELEASE_ASSET_MAX" | |||
| 68 | + | EnvReleaseAssetsMax = "ERMITE_RELEASE_ASSETS_MAX" | |||
| 69 | + | EnvReleaseQuota = "ERMITE_RELEASE_QUOTA" | |||
| 59 | ) | 70 | ) | ||
| 60 | 71 | ||||
| 61 | // SecretsKeyLen is the length of the secrets key in bytes (AES-256). | 72 | // SecretsKeyLen is the length of the secrets key in bytes (AES-256). | ||
| @@ -140,6 +151,12 @@ type Config struct { | |||||
| 140 | CIArtifactRetention time.Duration | 151 | CIArtifactRetention time.Duration | ||
| 141 | CIArtifactQuota int64 | 152 | CIArtifactQuota int64 | ||
| 142 | CIArtifactMax int64 | 153 | CIArtifactMax int64 | ||
| 154 | + | // S3Bucket selects S3; empty selects local disk. Credentials are secrets. | |||
| 155 | + | S3Bucket, S3Endpoint, S3Region, S3AccessKeyID, S3SecretAccessKey, S3Prefix string | |||
| 156 | + | S3ForcePathStyle bool | |||
| 157 | + | // Zero release limits leave defaults to the release use case. | |||
| 158 | + | ReleaseAssetMax, ReleaseQuota int64 | |||
| 159 | + | ReleaseAssetsMax int | |||
| 143 | } | 160 | } | ||
| 144 | 161 | ||||
| 145 | // Error lists every configuration problem found, in variable order. | 162 | // Error lists every configuration problem found, in variable order. | ||
| @@ -167,6 +184,7 @@ func Load(get func(string) string) (Config, error) { | |||||
| 167 | bad := func(key, format string, args ...any) { | 184 | bad := func(key, format string, args ...any) { | ||
| 168 | problems = append(problems, key+": "+fmt.Sprintf(format, args...)) | 185 | problems = append(problems, key+": "+fmt.Sprintf(format, args...)) | ||
| 169 | } | 186 | } | ||
| 187 | + | get = resolveFiles(get, &problems) | |||
| 170 | var c Config | 188 | var c Config | ||
| 171 | 189 | ||||
| 172 | c.Addr = get(EnvAddr) | 190 | c.Addr = get(EnvAddr) | ||
| @@ -342,12 +360,111 @@ func Load(get func(string) string) (Config, error) { | |||||
| 342 | } | 360 | } | ||
| 343 | } | 361 | } | ||
| 344 | 362 | ||||
| 363 | + | loadStorage(get, &c, &problems) | |||
| 345 | if len(problems) > 0 { | 364 | if len(problems) > 0 { | ||
| 346 | return Config{}, &Error{Problems: problems} | 365 | return Config{}, &Error{Problems: problems} | ||
| 347 | } | 366 | } | ||
| 348 | return c, nil | 367 | return c, nil | ||
| 349 | } | 368 | } | ||
| 350 | 369 | ||||
| 370 | + | // resolveFiles resolves only the four supported secrets, once at startup. | |||
| 371 | + | // Neither file paths, file errors nor secret contents enter diagnostics. | |||
| 372 | + | func resolveFiles(get func(string) string, problems *[]string) func(string) string { | |||
| 373 | + | values := make(map[string]string) | |||
| 374 | + | for _, key := range []string{EnvAdminToken, EnvSecretsKey, EnvS3AccessKeyID, EnvS3SecretAccessKey} { | |||
| 375 | + | value, file := get(key), get(key+"_FILE") | |||
| 376 | + | if value != "" && file != "" { | |||
| 377 | + | *problems = append(*problems, key+": cannot set both variable and _FILE") | |||
| 378 | + | } else if file != "" { | |||
| 379 | + | data, err := os.ReadFile(file) | |||
| 380 | + | if err != nil { | |||
| 381 | + | *problems = append(*problems, key+"_FILE: cannot read secret file") | |||
| 382 | + | } else { | |||
| 383 | + | value = strings.TrimRight(string(data), "\r\n") | |||
| 384 | + | if value == "" { | |||
| 385 | + | *problems = append(*problems, key+"_FILE: secret file is empty") | |||
| 386 | + | } | |||
| 387 | + | } | |||
| 388 | + | } | |||
| 389 | + | values[key] = value | |||
| 390 | + | } | |||
| 391 | + | return func(key string) string { | |||
| 392 | + | if value, ok := values[key]; ok { | |||
| 393 | + | return value | |||
| 394 | + | } | |||
| 395 | + | return get(key) | |||
| 396 | + | } | |||
| 397 | + | } | |||
| 398 | + | ||||
| 399 | + | func loadStorage(get func(string) string, c *Config, problems *[]string) { | |||
| 400 | + | bad := func(key, message string) { *problems = append(*problems, key+": "+message) } | |||
| 401 | + | c.S3Bucket = get(EnvS3Bucket) | |||
| 402 | + | c.S3Endpoint = get(EnvS3Endpoint) | |||
| 403 | + | c.S3Region = get(EnvS3Region) | |||
| 404 | + | c.S3AccessKeyID = get(EnvS3AccessKeyID) | |||
| 405 | + | c.S3SecretAccessKey = get(EnvS3SecretAccessKey) | |||
| 406 | + | c.S3Prefix = strings.Trim(get(EnvS3Prefix), "/") | |||
| 407 | + | if c.S3Prefix != "" { | |||
| 408 | + | valid := len(c.S3Prefix) <= 512 | |||
| 409 | + | for _, part := range strings.Split(c.S3Prefix, "/") { | |||
| 410 | + | if part == "" || part == "." || part == ".." { | |||
| 411 | + | valid = false | |||
| 412 | + | } | |||
| 413 | + | } | |||
| 414 | + | for _, r := range c.S3Prefix { | |||
| 415 | + | if r < 0x21 || r > 0x7e || r == '\\' { | |||
| 416 | + | valid = false | |||
| 417 | + | } | |||
| 418 | + | } | |||
| 419 | + | if !valid { | |||
| 420 | + | bad(EnvS3Prefix, "must be an ASCII key prefix without empty, dot or dot-dot segments") | |||
| 421 | + | } | |||
| 422 | + | c.S3Prefix += "/" | |||
| 423 | + | } | |||
| 424 | + | if c.S3Endpoint != "" { | |||
| 425 | + | if endpoint, err := parseURL(c.S3Endpoint, false); err != nil { | |||
| 426 | + | bad(EnvS3Endpoint, "must be an http(s) URL without credentials, path, query or fragment") | |||
| 427 | + | } else { | |||
| 428 | + | c.S3Endpoint = endpoint | |||
| 429 | + | } | |||
| 430 | + | } | |||
| 431 | + | switch get(EnvS3ForcePathStyle) { | |||
| 432 | + | case "", "0", "false": | |||
| 433 | + | case "1", "true": | |||
| 434 | + | c.S3ForcePathStyle = true | |||
| 435 | + | default: | |||
| 436 | + | bad(EnvS3ForcePathStyle, "must be 0, 1, false or true") | |||
| 437 | + | } | |||
| 438 | + | if c.S3Bucket != "" { | |||
| 439 | + | for _, v := range []struct{ key, value string }{{EnvS3Region, c.S3Region}, {EnvS3AccessKeyID, c.S3AccessKeyID}, {EnvS3SecretAccessKey, c.S3SecretAccessKey}} { | |||
| 440 | + | if strings.TrimSpace(v.value) == "" { | |||
| 441 | + | bad(v.key, "required when ERMITE_S3_BUCKET is set") | |||
| 442 | + | } | |||
| 443 | + | } | |||
| 444 | + | } | |||
| 445 | + | for _, v := range []struct { | |||
| 446 | + | key string | |||
| 447 | + | dst *int64 | |||
| 448 | + | }{{EnvReleaseAssetMax, &c.ReleaseAssetMax}, {EnvReleaseQuota, &c.ReleaseQuota}} { | |||
| 449 | + | if raw := strings.TrimSpace(get(v.key)); raw != "" { | |||
| 450 | + | n, err := parseSize(raw) | |||
| 451 | + | if err != nil { | |||
| 452 | + | bad(v.key, err.Error()) | |||
| 453 | + | } else { | |||
| 454 | + | *v.dst = n | |||
| 455 | + | } | |||
| 456 | + | } | |||
| 457 | + | } | |||
| 458 | + | if raw := get(EnvReleaseAssetsMax); raw != "" { | |||
| 459 | + | n, err := strconv.Atoi(raw) | |||
| 460 | + | if err != nil || n < 1 { | |||
| 461 | + | bad(EnvReleaseAssetsMax, "must be a positive integer") | |||
| 462 | + | } else { | |||
| 463 | + | c.ReleaseAssetsMax = n | |||
| 464 | + | } | |||
| 465 | + | } | |||
| 466 | + | } | |||
| 467 | + | ||||
| 351 | // sizeUnits are the suffixes of parseSize, longest first. | 468 | // sizeUnits are the suffixes of parseSize, longest first. | ||
| 352 | var sizeUnits = []struct { | 469 | var sizeUnits = []struct { | ||
| 353 | suffix string | 470 | suffix string | ||
A internal/config/storage_test.go +111 -0
| @@ -0,0 +1,111 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | package config_test | |
| 3 | + | ||
| 4 | + | import ( | |
| 5 | + | "crypto/sha256" | |
| 6 | + | "encoding/base64" | |
| 7 | + | "os" | |
| 8 | + | "path/filepath" | |
| 9 | + | "strings" | |
| 10 | + | "testing" | |
| 11 | + | ||
| 12 | + | "bnema.dev/ermite/internal/config" | |
| 13 | + | "github.com/stretchr/testify/assert" | |
| 14 | + | "github.com/stretchr/testify/require" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | func s3Env() map[string]string { | |
| 18 | + | return map[string]string{ | |
| 19 | + | config.EnvS3Bucket: "bucket", config.EnvS3Region: "us-east-1", | |
| 20 | + | config.EnvS3AccessKeyID: "private-id", config.EnvS3SecretAccessKey: "private-secret", | |
| 21 | + | } | |
| 22 | + | } | |
| 23 | + | func TestStorageConfig(t *testing.T) { | |
| 24 | + | c, err := config.Load(lookup(valid(nil))) | |
| 25 | + | require.NoError(t, err) | |
| 26 | + | assert.Empty(t, c.S3Bucket) | |
| 27 | + | env := s3Env() | |
| 28 | + | env[config.EnvS3Endpoint] = "http://localhost:9000/" | |
| 29 | + | env[config.EnvS3Prefix] = "/forge/blobs/" | |
| 30 | + | env[config.EnvS3ForcePathStyle] = "1" | |
| 31 | + | env[config.EnvReleaseAssetMax] = "512MiB" | |
| 32 | + | env[config.EnvReleaseQuota] = "10GiB" | |
| 33 | + | env[config.EnvReleaseAssetsMax] = "7" | |
| 34 | + | c, err = config.Load(lookup(valid(env))) | |
| 35 | + | require.NoError(t, err) | |
| 36 | + | assert.Equal(t, "forge/blobs/", c.S3Prefix) | |
| 37 | + | assert.Equal(t, "http://localhost:9000", c.S3Endpoint) | |
| 38 | + | assert.True(t, c.S3ForcePathStyle) | |
| 39 | + | assert.EqualValues(t, 512<<20, c.ReleaseAssetMax) | |
| 40 | + | assert.EqualValues(t, 10<<30, c.ReleaseQuota) | |
| 41 | + | assert.Equal(t, 7, c.ReleaseAssetsMax) | |
| 42 | + | for _, key := range []string{config.EnvS3Region, config.EnvS3AccessKeyID, config.EnvS3SecretAccessKey} { | |
| 43 | + | t.Run(key, func(t *testing.T) { | |
| 44 | + | env := s3Env() | |
| 45 | + | env[key] = "" | |
| 46 | + | _, err := config.Load(lookup(valid(env))) | |
| 47 | + | require.Error(t, err) | |
| 48 | + | assert.Contains(t, err.Error(), key) | |
| 49 | + | assert.NotContains(t, err.Error(), "private-") | |
| 50 | + | }) | |
| 51 | + | } | |
| 52 | + | for key, value := range map[string]string{config.EnvS3Endpoint: "https://private-id:private-secret@example.org", config.EnvS3ForcePathStyle: "yes", config.EnvS3Prefix: "x/../y", config.EnvReleaseAssetMax: "0", config.EnvReleaseQuota: "-1", config.EnvReleaseAssetsMax: "0"} { | |
| 53 | + | t.Run(key+" invalid", func(t *testing.T) { | |
| 54 | + | env := s3Env() | |
| 55 | + | env[key] = value | |
| 56 | + | _, err := config.Load(lookup(valid(env))) | |
| 57 | + | require.Error(t, err) | |
| 58 | + | assert.Contains(t, err.Error(), key) | |
| 59 | + | assert.NotContains(t, err.Error(), "private-") | |
| 60 | + | }) | |
| 61 | + | } | |
| 62 | + | for _, value := range []string{"0", "1", "false", "true"} { | |
| 63 | + | env := s3Env() | |
| 64 | + | env[config.EnvS3ForcePathStyle] = value | |
| 65 | + | _, err := config.Load(lookup(valid(env))) | |
| 66 | + | require.NoError(t, err) | |
| 67 | + | } | |
| 68 | + | } | |
| 69 | + | func TestSecretFiles(t *testing.T) { | |
| 70 | + | for _, key := range []string{config.EnvAdminToken, config.EnvSecretsKey, config.EnvS3AccessKeyID, config.EnvS3SecretAccessKey} { | |
| 71 | + | t.Run(key, func(t *testing.T) { | |
| 72 | + | secret := strings.Repeat("s", 32) | |
| 73 | + | if key == config.EnvSecretsKey { | |
| 74 | + | secret = base64.StdEncoding.EncodeToString([]byte(secret)) | |
| 75 | + | } | |
| 76 | + | path := filepath.Join(t.TempDir(), "secret") | |
| 77 | + | require.NoError(t, os.WriteFile(path, []byte(secret+"\r\n"), 0600)) | |
| 78 | + | env := valid(s3Env()) | |
| 79 | + | env[key] = "" | |
| 80 | + | env[key+"_FILE"] = path | |
| 81 | + | c, err := config.Load(lookup(env)) | |
| 82 | + | require.NoError(t, err) | |
| 83 | + | switch key { | |
| 84 | + | case config.EnvAdminToken: | |
| 85 | + | assert.Equal(t, sha256.Sum256([]byte(secret)), c.AdminTokenHash) | |
| 86 | + | case config.EnvSecretsKey: | |
| 87 | + | assert.Len(t, c.SecretsKey, 32) | |
| 88 | + | case config.EnvS3AccessKeyID: | |
| 89 | + | assert.Equal(t, secret, c.S3AccessKeyID) | |
| 90 | + | case config.EnvS3SecretAccessKey: | |
| 91 | + | assert.Equal(t, secret, c.S3SecretAccessKey) | |
| 92 | + | } | |
| 93 | + | env[key] = secret | |
| 94 | + | _, err = config.Load(lookup(env)) | |
| 95 | + | require.Error(t, err) | |
| 96 | + | assert.Contains(t, err.Error(), "both") | |
| 97 | + | assert.NotContains(t, err.Error(), secret) | |
| 98 | + | env[key] = "" | |
| 99 | + | env[key+"_FILE"] = path + "missing" | |
| 100 | + | _, err = config.Load(lookup(env)) | |
| 101 | + | require.Error(t, err) | |
| 102 | + | assert.Contains(t, err.Error(), key+"_FILE") | |
| 103 | + | assert.NotContains(t, err.Error(), path) | |
| 104 | + | require.NoError(t, os.WriteFile(path, []byte("\n"), 0600)) | |
| 105 | + | env[key+"_FILE"] = path | |
| 106 | + | _, err = config.Load(lookup(env)) | |
| 107 | + | require.Error(t, err) | |
| 108 | + | assert.Contains(t, err.Error(), "empty") | |
| 109 | + | }) | |
| 110 | + | } | |
| 111 | + | } | |
| @@ -0,0 +1,111 @@ | |||||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |||
| 2 | + | package config_test | |||
| 3 | + | ||||
| 4 | + | import ( | |||
| 5 | + | "crypto/sha256" | |||
| 6 | + | "encoding/base64" | |||
| 7 | + | "os" | |||
| 8 | + | "path/filepath" | |||
| 9 | + | "strings" | |||
| 10 | + | "testing" | |||
| 11 | + | ||||
| 12 | + | "bnema.dev/ermite/internal/config" | |||
| 13 | + | "github.com/stretchr/testify/assert" | |||
| 14 | + | "github.com/stretchr/testify/require" | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | func s3Env() map[string]string { | |||
| 18 | + | return map[string]string{ | |||
| 19 | + | config.EnvS3Bucket: "bucket", config.EnvS3Region: "us-east-1", | |||
| 20 | + | config.EnvS3AccessKeyID: "private-id", config.EnvS3SecretAccessKey: "private-secret", | |||
| 21 | + | } | |||
| 22 | + | } | |||
| 23 | + | func TestStorageConfig(t *testing.T) { | |||
| 24 | + | c, err := config.Load(lookup(valid(nil))) | |||
| 25 | + | require.NoError(t, err) | |||
| 26 | + | assert.Empty(t, c.S3Bucket) | |||
| 27 | + | env := s3Env() | |||
| 28 | + | env[config.EnvS3Endpoint] = "http://localhost:9000/" | |||
| 29 | + | env[config.EnvS3Prefix] = "/forge/blobs/" | |||
| 30 | + | env[config.EnvS3ForcePathStyle] = "1" | |||
| 31 | + | env[config.EnvReleaseAssetMax] = "512MiB" | |||
| 32 | + | env[config.EnvReleaseQuota] = "10GiB" | |||
| 33 | + | env[config.EnvReleaseAssetsMax] = "7" | |||
| 34 | + | c, err = config.Load(lookup(valid(env))) | |||
| 35 | + | require.NoError(t, err) | |||
| 36 | + | assert.Equal(t, "forge/blobs/", c.S3Prefix) | |||
| 37 | + | assert.Equal(t, "http://localhost:9000", c.S3Endpoint) | |||
| 38 | + | assert.True(t, c.S3ForcePathStyle) | |||
| 39 | + | assert.EqualValues(t, 512<<20, c.ReleaseAssetMax) | |||
| 40 | + | assert.EqualValues(t, 10<<30, c.ReleaseQuota) | |||
| 41 | + | assert.Equal(t, 7, c.ReleaseAssetsMax) | |||
| 42 | + | for _, key := range []string{config.EnvS3Region, config.EnvS3AccessKeyID, config.EnvS3SecretAccessKey} { | |||
| 43 | + | t.Run(key, func(t *testing.T) { | |||
| 44 | + | env := s3Env() | |||
| 45 | + | env[key] = "" | |||
| 46 | + | _, err := config.Load(lookup(valid(env))) | |||
| 47 | + | require.Error(t, err) | |||
| 48 | + | assert.Contains(t, err.Error(), key) | |||
| 49 | + | assert.NotContains(t, err.Error(), "private-") | |||
| 50 | + | }) | |||
| 51 | + | } | |||
| 52 | + | for key, value := range map[string]string{config.EnvS3Endpoint: "https://private-id:private-secret@example.org", config.EnvS3ForcePathStyle: "yes", config.EnvS3Prefix: "x/../y", config.EnvReleaseAssetMax: "0", config.EnvReleaseQuota: "-1", config.EnvReleaseAssetsMax: "0"} { | |||
| 53 | + | t.Run(key+" invalid", func(t *testing.T) { | |||
| 54 | + | env := s3Env() | |||
| 55 | + | env[key] = value | |||
| 56 | + | _, err := config.Load(lookup(valid(env))) | |||
| 57 | + | require.Error(t, err) | |||
| 58 | + | assert.Contains(t, err.Error(), key) | |||
| 59 | + | assert.NotContains(t, err.Error(), "private-") | |||
| 60 | + | }) | |||
| 61 | + | } | |||
| 62 | + | for _, value := range []string{"0", "1", "false", "true"} { | |||
| 63 | + | env := s3Env() | |||
| 64 | + | env[config.EnvS3ForcePathStyle] = value | |||
| 65 | + | _, err := config.Load(lookup(valid(env))) | |||
| 66 | + | require.NoError(t, err) | |||
| 67 | + | } | |||
| 68 | + | } | |||
| 69 | + | func TestSecretFiles(t *testing.T) { | |||
| 70 | + | for _, key := range []string{config.EnvAdminToken, config.EnvSecretsKey, config.EnvS3AccessKeyID, config.EnvS3SecretAccessKey} { | |||
| 71 | + | t.Run(key, func(t *testing.T) { | |||
| 72 | + | secret := strings.Repeat("s", 32) | |||
| 73 | + | if key == config.EnvSecretsKey { | |||
| 74 | + | secret = base64.StdEncoding.EncodeToString([]byte(secret)) | |||
| 75 | + | } | |||
| 76 | + | path := filepath.Join(t.TempDir(), "secret") | |||
| 77 | + | require.NoError(t, os.WriteFile(path, []byte(secret+"\r\n"), 0600)) | |||
| 78 | + | env := valid(s3Env()) | |||
| 79 | + | env[key] = "" | |||
| 80 | + | env[key+"_FILE"] = path | |||
| 81 | + | c, err := config.Load(lookup(env)) | |||
| 82 | + | require.NoError(t, err) | |||
| 83 | + | switch key { | |||
| 84 | + | case config.EnvAdminToken: | |||
| 85 | + | assert.Equal(t, sha256.Sum256([]byte(secret)), c.AdminTokenHash) | |||
| 86 | + | case config.EnvSecretsKey: | |||
| 87 | + | assert.Len(t, c.SecretsKey, 32) | |||
| 88 | + | case config.EnvS3AccessKeyID: | |||
| 89 | + | assert.Equal(t, secret, c.S3AccessKeyID) | |||
| 90 | + | case config.EnvS3SecretAccessKey: | |||
| 91 | + | assert.Equal(t, secret, c.S3SecretAccessKey) | |||
| 92 | + | } | |||
| 93 | + | env[key] = secret | |||
| 94 | + | _, err = config.Load(lookup(env)) | |||
| 95 | + | require.Error(t, err) | |||
| 96 | + | assert.Contains(t, err.Error(), "both") | |||
| 97 | + | assert.NotContains(t, err.Error(), secret) | |||
| 98 | + | env[key] = "" | |||
| 99 | + | env[key+"_FILE"] = path + "missing" | |||
| 100 | + | _, err = config.Load(lookup(env)) | |||
| 101 | + | require.Error(t, err) | |||
| 102 | + | assert.Contains(t, err.Error(), key+"_FILE") | |||
| 103 | + | assert.NotContains(t, err.Error(), path) | |||
| 104 | + | require.NoError(t, os.WriteFile(path, []byte("\n"), 0600)) | |||
| 105 | + | env[key+"_FILE"] = path | |||
| 106 | + | _, err = config.Load(lookup(env)) | |||
| 107 | + | require.Error(t, err) | |||
| 108 | + | assert.Contains(t, err.Error(), "empty") | |||
| 109 | + | }) | |||
| 110 | + | } | |||
| 111 | + | } | |||
M skills/ermite/CLI.md +20 -0
| @@ -44,6 +44,7 @@ Scopes: | |||
| 44 | 44 | | `mr:merge` | merge, and delete the merged source branch with `--delete-branch` | | |
| 45 | 45 | | `mr:force-merge` | bypass failing or unfinished checks with `--ignore-checks` | | |
| 46 | 46 | | `issue:write` | open, comment on, close and reopen issues | | |
| 47 | + | | `release:write` | create, edit and delete releases and their assets; included in `cli`; refused for branch-limited tokens | | |
| 47 | 48 | | `token:admin` | create, list and revoke tokens | | |
| 48 | 49 | | `ci:write`, `secrets:write`, `runner:register` | reserved for CI | | |
| 49 | 50 | ||
| @@ -93,6 +94,25 @@ Issues share the numbering of merge requests and must be enabled per repository: | |||
| 93 | 94 | ||
| 94 | 95 | Issue JSON: `repo`, `number`, `title`, `body`, `state`, `author` (token label), `created_at`, `updated_at`, `comment_count`. `issue show --json` adds `comments` (`id`, `author`, `text`, `created_at`). `repo show --json` has `issues`. | |
| 95 | 96 | ||
| 97 | + | ## Releases | |
| 98 | + | ||
| 99 | + | Needs the `releases` server feature. Read access follows repository visibility; | |
| 100 | + | drafts need write access. Writes need `release:write`, never a branch-limited | |
| 101 | + | token, or a CI job's `contents: write` permission. Select a release by tag or id; | |
| 102 | + | `view` and `download` also accept `latest`. | |
| 103 | + | ||
| 104 | + | | Command | Notes | | |
| 105 | + | |---|---| | |
| 106 | + | | `release create <repo> <tag> [--title T] [--notes TEXT\|--notes-file F\|-] [--target REF] [--draft] [--prerelease] [--latest] [--asset FILE[#LABEL]] [--json]` | existing tags never move; target creates a missing tag | | |
| 107 | + | | `release list <repo> [--limit N] [--json]` | newest first | | |
| 108 | + | | `release view <repo> <tag\|id\|latest> [--json\|--notes]` | metadata and assets | | |
| 109 | + | | `release edit <repo> <tag\|id> [--title T] [--notes TEXT\|--notes-file F\|-] [--tag T] [--target REF] [--draft\|--publish] [--prerelease\|--no-prerelease] [--latest] [--json]` | only supplied fields change | | |
| 110 | + | | `release delete <repo> <tag\|id> [--yes]` | leaves the Git tag | | |
| 111 | + | | `release upload <repo> <tag\|id> <file[#label]>… [--clobber] [--content-type TYPE] [--json]` | clobber replaces same-named assets | | |
| 112 | + | | `release download <repo> <tag\|id\|latest> [--name N]… [--pattern GLOB]… [--dir D]` | refuses existing local files | | |
| 113 | + | ||
| 114 | + | See [publishing and storage](../../docs/releases.md). | |
| 115 | + | ||
| 96 | 116 | ## Workflows | |
| 97 | 117 | ||
| 98 | 118 | | Command | Scope | Notes | | |
| @@ -44,6 +44,7 @@ Scopes: | |||||
| 44 | | `mr:merge` | merge, and delete the merged source branch with `--delete-branch` | | 44 | | `mr:merge` | merge, and delete the merged source branch with `--delete-branch` | | ||
| 45 | | `mr:force-merge` | bypass failing or unfinished checks with `--ignore-checks` | | 45 | | `mr:force-merge` | bypass failing or unfinished checks with `--ignore-checks` | | ||
| 46 | | `issue:write` | open, comment on, close and reopen issues | | 46 | | `issue:write` | open, comment on, close and reopen issues | | ||
| 47 | + | | `release:write` | create, edit and delete releases and their assets; included in `cli`; refused for branch-limited tokens | | |||
| 47 | | `token:admin` | create, list and revoke tokens | | 48 | | `token:admin` | create, list and revoke tokens | | ||
| 48 | | `ci:write`, `secrets:write`, `runner:register` | reserved for CI | | 49 | | `ci:write`, `secrets:write`, `runner:register` | reserved for CI | | ||
| 49 | 50 | ||||
| @@ -93,6 +94,25 @@ Issues share the numbering of merge requests and must be enabled per repository: | |||||
| 93 | 94 | ||||
| 94 | Issue JSON: `repo`, `number`, `title`, `body`, `state`, `author` (token label), `created_at`, `updated_at`, `comment_count`. `issue show --json` adds `comments` (`id`, `author`, `text`, `created_at`). `repo show --json` has `issues`. | 95 | Issue JSON: `repo`, `number`, `title`, `body`, `state`, `author` (token label), `created_at`, `updated_at`, `comment_count`. `issue show --json` adds `comments` (`id`, `author`, `text`, `created_at`). `repo show --json` has `issues`. | ||
| 95 | 96 | ||||
| 97 | + | ## Releases | |||
| 98 | + | ||||
| 99 | + | Needs the `releases` server feature. Read access follows repository visibility; | |||
| 100 | + | drafts need write access. Writes need `release:write`, never a branch-limited | |||
| 101 | + | token, or a CI job's `contents: write` permission. Select a release by tag or id; | |||
| 102 | + | `view` and `download` also accept `latest`. | |||
| 103 | + | ||||
| 104 | + | | Command | Notes | | |||
| 105 | + | |---|---| | |||
| 106 | + | | `release create <repo> <tag> [--title T] [--notes TEXT\|--notes-file F\|-] [--target REF] [--draft] [--prerelease] [--latest] [--asset FILE[#LABEL]] [--json]` | existing tags never move; target creates a missing tag | | |||
| 107 | + | | `release list <repo> [--limit N] [--json]` | newest first | | |||
| 108 | + | | `release view <repo> <tag\|id\|latest> [--json\|--notes]` | metadata and assets | | |||
| 109 | + | | `release edit <repo> <tag\|id> [--title T] [--notes TEXT\|--notes-file F\|-] [--tag T] [--target REF] [--draft\|--publish] [--prerelease\|--no-prerelease] [--latest] [--json]` | only supplied fields change | | |||
| 110 | + | | `release delete <repo> <tag\|id> [--yes]` | leaves the Git tag | | |||
| 111 | + | | `release upload <repo> <tag\|id> <file[#label]>… [--clobber] [--content-type TYPE] [--json]` | clobber replaces same-named assets | | |||
| 112 | + | | `release download <repo> <tag\|id\|latest> [--name N]… [--pattern GLOB]… [--dir D]` | refuses existing local files | | |||
| 113 | + | ||||
| 114 | + | See [publishing and storage](../../docs/releases.md). | |||
| 115 | + | ||||
| 96 | ## Workflows | 116 | ## Workflows | ||
| 97 | 117 | ||||
| 98 | | Command | Scope | Notes | | 118 | | Command | Scope | Notes | | ||