From cfb3337d44fb66211abd75b234dd268cf6de803f Mon Sep 17 00:00:00 2001 From: William Smith Date: Thu, 1 Oct 2026 10:40:27 -0700 Subject: [PATCH] ci: build, sign and release ROneCOne.cls from a pushed tag Pushing a vX.Y.Z tag now runs Publish. It refuses a tag that is not the version in the release header of src/ROneCOne.cls, restamps the release headers and fails if that changes a committed module, and writes ROneCOne.cls (with the CRLF line endings every release has shipped) and vX.Y.Z-sha256.txt. Security and Malware scan run on the tagged commit, the security report is written over the built file, and the release is created with both files, their signed provenance and the report. The notes are docs/releases/vX.Y.Z.md. Releases no longer attach the demo workbooks; they stay in demo/, and the docs point there. release-security.yml is gone. --- .github/workflows/malware-scan.yml | 8 +- .github/workflows/publish.yml | 262 +++++++++++++++++++++++++ .github/workflows/release-security.yml | 73 ------- .github/workflows/security.yml | 10 +- .github/zizmor.yml | 12 ++ AGENTS.md | 20 +- CHANGELOG.md | 3 + README.md | 4 +- SECURITY.md | 53 +++-- docs/README.md | 3 +- docs/development.md | 6 +- docs/user-guide/README.md | 3 +- docs/user-guide/excel-tables.md | 4 +- docs/user-guide/getting-started.md | 4 +- 14 files changed, 356 insertions(+), 109 deletions(-) create mode 100644 .github/workflows/publish.yml delete mode 100644 .github/workflows/release-security.yml create mode 100644 .github/zizmor.yml diff --git a/.github/workflows/malware-scan.yml b/.github/workflows/malware-scan.yml index 5eb945e..9639aaf 100644 --- a/.github/workflows/malware-scan.yml +++ b/.github/workflows/malware-scan.yml @@ -2,21 +2,25 @@ name: Malware scan # ClamAV and YARA-X over the shipped runtime and demo workbooks, one job each. # tools/malware_scan.py --only picks the scanner and checks only that -# scanner's exceptions in tools/malware_exceptions.json. +# scanner's exceptions in tools/malware_exceptions.json. Publish calls this +# workflow, so a release waits on it. on: push: branches: [main] pull_request: workflow_dispatch: + workflow_call: schedule: - cron: '17 19 * * *' permissions: contents: read +# The event is in the group so a Publish dry run on main, which calls this +# workflow, neither cancels nor is cancelled by a push run on main. concurrency: - group: malware-scan-${{ github.ref }} + group: malware-scan-${{ github.event_name }}-${{ github.ref }} cancel-in-progress: true jobs: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..f22249a --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,262 @@ +name: Publish + +# A v*.*.* tag releases ROneCOne. The build job refuses a tag that does not +# match the release header in src/ROneCOne.cls, restamps every module and +# fails if that changes a committed file, then writes the release files: +# ROneCOne.cls and vX.Y.Z-sha256.txt. Security and Malware scan run on the +# tagged commit, the security report is written over the built file, this +# workflow signs the build provenance, and it creates the GitHub release with +# the release files, the signed bundle and the report. The notes are +# docs/releases/vX.Y.Z.md. The demo workbooks are not attached; they stay in +# demo/. +# +# Started by hand, this is always a dry run: it builds, scans and assembles +# the release files exactly as a release would, for the version in the +# release header, and uploads them as the release-preview artifact instead of +# signing or releasing anything. + +"on": + push: + tags: + - "v*.*.*" + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: Build the release files + runs-on: ubuntu-24.04 + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12.14" + + # First, so a mistyped tag fails in seconds. The release header is the + # version the runtime itself carries; the restamp below proves it is the + # newest dated version in CHANGELOG.md. + - name: Read the version from the release header + id: version + run: | + VERSION=$(sed -n "s/^' ROneCOne \([0-9]*\.[0-9]*\.[0-9]*\), released [0-9-]*\r\{0,1\}$/\1/p" src/ROneCOne.cls) + if [ "$(printf '%s\n' "$VERSION" | grep -c .)" != 1 ]; then + echo "::error::src/ROneCOne.cls has no single release header." + exit 1 + fi + if [ "$GITHUB_REF_TYPE" = tag ] && [ "${GITHUB_REF_NAME#v}" != "$VERSION" ]; then + echo "::error::Tag $GITHUB_REF_NAME does not match the release header $VERSION." + exit 1 + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Version $VERSION" + + # The release ships the committed modules exactly. Restamping is pure + # text and changes nothing when every header already matches CHANGELOG.md + # and LICENSE. + - name: Check the committed modules are stamped + run: | + python tools/stamp_release_headers.py + git diff --exit-code + + - name: Check the release notes exist + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + if [ ! -s "docs/releases/v$VERSION.md" ]; then + echo "::error::docs/releases/v$VERSION.md is missing or empty." + exit 1 + fi + + # Git stores the module with LF line endings. Every release has shipped + # it as a Windows checkout writes it, with CRLF, which is the form the + # Visual Basic Editor's Import File reads; only the line endings change. + # A committed docs/releases/vX.Y.Z-sha256.txt must agree with the one + # written here. + - name: Write the release files + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + mkdir release-files + python -c 'import sys; data = open(sys.argv[1], "rb").read(); assert b"\r" not in data, "CR in the stored module"; open(sys.argv[2], "wb").write(data.replace(b"\n", b"\r\n"))' src/ROneCOne.cls release-files/ROneCOne.cls + (cd release-files && sha256sum ROneCOne.cls > "v$VERSION-sha256.txt") + cat "release-files/v$VERSION-sha256.txt" + committed="docs/releases/v$VERSION-sha256.txt" + if [ -f "$committed" ] && ! diff "$committed" "release-files/v$VERSION-sha256.txt"; then + echo "::error::$committed does not match the file built here." + exit 1 + fi + + - name: Keep the release files + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: release-files + path: release-files/ + if-no-files-found: error + + security: + name: Security + uses: ./.github/workflows/security.yml + permissions: + contents: read + security-events: write + + malware-scan: + name: Malware scan + uses: ./.github/workflows/malware-scan.yml + permissions: + contents: read + + # olevba and mraptor over the built file, compared with the reviewed + # baseline at this commit: what each release has carried since 1.10.2. + security-report: + name: Write the security report + needs: build + runs-on: ubuntu-24.04 + env: + VERSION: ${{ needs.build.outputs.version }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12.14" + + - name: Install analysis tooling + run: python -m pip install --require-hashes -r .github/requirements/security.txt + + - name: Download the release files + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-files + path: release-files/ + + - name: Write the report + run: >- + python tools/security_scan.py + --report "security-report/v$VERSION-security-report.md" + --title "Security report for ROneCOne v$VERSION" + release-files/ROneCOne.cls + + - name: Keep the report + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: security-report + path: security-report/ + if-no-files-found: error + + publish: + name: Attest the release files + needs: [build, security, malware-scan, security-report] + if: ${{ startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch' }} + runs-on: ubuntu-24.04 + permissions: + id-token: write # sign the attestation + attestations: write # store it + steps: + - name: Download the release files + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-files + path: release-files/ + + # Signed build provenance: `gh attestation verify --repo + # WilliamSmithEdward/ROneCOne` shows which commit and run built a file. + - name: Attest the release files + id: attest + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-path: release-files/* + + # The signed bundle goes on the GitHub release too, so a download can be + # checked offline: gh attestation verify --bundle . + - name: Keep the provenance bundle + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: provenance + path: ${{ steps.attest.outputs.bundle-path }} + if-no-files-found: error + + # On a dry run `publish` is skipped; this job still assembles the release + # and uploads it as an artifact, and only the last step is skipped. + github-release: + name: Create GitHub release + needs: [build, security, malware-scan, security-report, publish] + if: >- + ${{ !cancelled() && needs.build.result == 'success' + && needs.security.result == 'success' && needs.malware-scan.result == 'success' + && needs.security-report.result == 'success' + && (needs.publish.result == 'success' || github.event_name == 'workflow_dispatch') }} + runs-on: ubuntu-24.04 + permissions: + contents: write + env: + DRY_RUN: ${{ github.event_name == 'workflow_dispatch' }} + VERSION: ${{ needs.build.outputs.version }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Download the release files + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-files + path: release/ + + - name: Download the security report + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: security-report + path: release/ + + - name: Download the provenance bundle + if: env.DRY_RUN != 'true' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: provenance + path: provenance/ + + - name: Name the bundle after the release + if: env.DRY_RUN != 'true' + run: | + test "$(find provenance -type f | wc -l)" = 1 + mv provenance/* "release/ROneCOne-$VERSION.sigstore.json" + + - name: Take the release notes + run: | + cp "docs/releases/v$VERSION.md" release_notes.md + if [ ! -s release_notes.md ]; then + echo "::error::docs/releases/v$VERSION.md is empty." + exit 1 + fi + ls -l release + + - name: Keep what the release would carry + if: env.DRY_RUN == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: release-preview + path: | + release/ + release_notes.md + if-no-files-found: error + + - name: Create GitHub release + if: env.DRY_RUN != 'true' + uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 + with: + # The title is the tag and nothing else. The headline is in the body. + name: ${{ github.ref_name }} + tag_name: ${{ github.ref_name }} + body_path: release_notes.md + files: release/* + fail_on_unmatched_files: true diff --git a/.github/workflows/release-security.yml b/.github/workflows/release-security.yml deleted file mode 100644 index 22916db..0000000 --- a/.github/workflows/release-security.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: Release security report - -# Each published release gets an olevba and mraptor report on its own assets: -# the runtime and every demo workbook, their SHA-256 hashes, and how their -# results compare with the reviewed baseline at the release tag. The report is -# attached to the release as -security-report.md. -# -# Started by hand with a release's tag it is always a dry run: it runs the -# scanner as it stands on the branch it was started from, which is what the -# next release will run, over that release's assets, and uploads the report -# as the release-preview artifact instead of attaching it. - -on: - release: - types: [published] - workflow_dispatch: - inputs: - tag: - description: Release tag to report on (a dry run), such as v1.10.2 - required: true - -permissions: - contents: read - -concurrency: - group: release-security-${{ github.event.release.tag_name || inputs.tag }} - -jobs: - release-report: - name: Attach release report - runs-on: ubuntu-24.04 - permissions: - contents: write # upload the report to the release - env: - TAG: ${{ github.event.release.tag_name || inputs.tag }} - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - ref: ${{ github.event.release.tag_name || github.sha }} - persist-credentials: false - - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 - with: - python-version: '3.12.14' - - - name: Install analysis tooling - run: python -m pip install --require-hashes -r .github/requirements/security.txt - - - name: Download the release assets - run: >- - gh release download "$TAG" --dir release-assets - --pattern '*.cls' --pattern '*.xlsm' - - - name: Write the report - run: >- - python tools/security_scan.py - --report "release-assets/$TAG-security-report.md" - --title "Security report for ROneCOne $TAG" - release-assets/*.cls release-assets/*.xlsm - - - name: Keep what the release would carry - if: ${{ !cancelled() && github.event_name != 'release' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: release-preview - path: "release-assets/${{ env.TAG }}-security-report.md" - if-no-files-found: error - - - name: Attach the report to the release - if: github.event_name == 'release' - run: gh release upload "$TAG" "release-assets/$TAG-security-report.md" --clobber diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index bd53b3a..c7b711e 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -5,8 +5,9 @@ name: Security # scan holds each shipped file's olevba findings and mraptor flags to # tools/security_baseline.json and fails on any change in either direction, on # code that runs on its own, and on P-code that its source does not explain. -# The release report lives in release-security.yml, the one workflow that needs -# write access. ClamAV and YARA-X run in malware-scan.yml. +# Publish calls this workflow, so a release waits on it, and writes the +# release's security report over the file it builds. ClamAV and YARA-X run in +# malware-scan.yml. # zizmor audits the workflows themselves and keeps its own exceptions (see its job). on: @@ -14,14 +15,17 @@ on: branches: [main] pull_request: workflow_dispatch: + workflow_call: schedule: - cron: '17 19 * * *' permissions: contents: read +# The event is in the group so a Publish dry run on main, which calls this +# workflow, neither cancels nor is cancelled by a push run on main. concurrency: - group: security-${{ github.ref }} + group: security-${{ github.event_name }}-${{ github.ref }} cancel-in-progress: true jobs: diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 0000000..cea683c --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,12 @@ +# zizmor audits the workflows in the Security workflow. Each rule turned off +# here says why and when it comes back. +rules: + # Asks for GitHub's `$/` self-repository syntax in `uses:` (July 2026). + # Adopted once GitHub's own documentation confirms it for called reusable + # workflows. + self-repository: + disable: true + # Asks for `gh release create` in place of softprops/action-gh-release in + # publish.yml. The release path is changed only after it can be dry-run. + superfluous-actions: + disable: true diff --git a/AGENTS.md b/AGENTS.md index 128e2c1..9ebdc77 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -96,6 +96,24 @@ visual review of every demo worksheet. Preserve ASCII VBA sources, 100-character source lines, MIT licensing, local-only diagnostics, and exact task-owned Excel process cleanup. +### Releasing + +A release carries `ROneCOne.cls`, `vX.Y.Z-sha256.txt`, the security report, and the signed +provenance bundle. The demo workbooks are not attached; users take them from `demo/`. + +1. Date the version's section in `CHANGELOG.md`, write its notes in `docs/releases/vX.Y.Z.md`, + restamp with `tools\stamp_release_headers.py`, repackage the demo workbooks with + `tools\package_demo_workbook.py` so `demo/` carries the same modules, and run the gates above. +2. Merge that to `main` through a pull request. +3. Optionally dry-run Publish: `gh workflow run publish.yml --ref main`, then + `gh run download -n release-preview`. +4. The owner pushes the `vX.Y.Z` tag on that commit. Publish refuses a tag that differs from the + release header in `src/ROneCOne.cls` or a module the restamp would change, writes the release + files from the commit, and creates the release with `docs/releases/vX.Y.Z.md` as its notes. + +Publish writes `vX.Y.Z-sha256.txt`, so there is no need to commit one to `docs/releases/`; one +that is committed must match the file Publish builds, or the release stops. + ## Excel and VBA Authority The user has authorized project-scoped use of these tools for Excel workbook and VBA work: @@ -449,7 +467,7 @@ limits carry the result. These rules are the same in every WilliamSmithEdward repository. -- **How a release happens here:** publishing a GitHub release starts the release reports, which scan it and attach their reports to it. Any other step, such as a marketplace upload, is described elsewhere in this file. +- **How a release happens here:** pushing a `vX.Y.Z` tag runs Publish, which builds the release files in CI and creates the GitHub release with them, their signed provenance and the security reports. Any other step, such as a marketplace upload, is described elsewhere in this file. - **Starting a workflow by hand never releases anything.** Publish and every release report are dry runs when started with `gh workflow run` or the Run workflow button. They build, scan and assemble the release files exactly diff --git a/CHANGELOG.md b/CHANGELOG.md index 0685734..84a77c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ checksums for each version are on the weekly, and the README shows its badge. - The release security workflow grants write access to the one job that uploads the report, rather than to the whole workflow. +- Releases are built and signed in CI: pushing a `vX.Y.Z` tag builds `ROneCOne.cls` and its + SHA-256 file, signs their build provenance, writes the security report, and creates the release. + Releases no longer attach the demo workbooks; they stay in `demo/`. - `requirements-dev.txt` installs on Windows again: it had been compiled without pywin32 and win-unicode-console, so `--require-hashes` refused it there. Recompiled with its own command. No change to ROneCOne itself. diff --git a/README.md b/README.md index 2da1b59..db19e9f 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,8 @@ only by the HTTP client, and only for URLs you request. It never launches a seco ## Start in three steps -1. **[Run a demo](https://github.com/WilliamSmithEdward/ROneCOne/releases/latest)** - each - release ships self-contained demo workbooks with visible, worksheet-by-worksheet results. +1. **[Run a demo](demo/)** - the `demo/` folder holds self-contained demo workbooks with + visible, worksheet-by-worksheet results. 2. **[Follow the getting-started guide](docs/user-guide/getting-started.md)** - import one class file and run your first query in about five minutes. 3. **[Pick a recipe](docs/user-guide/README.md)** - short guides cover collections, delegates, diff --git a/SECURITY.md b/SECURITY.md index 9db3bd8..7e719f7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -82,11 +82,9 @@ severity. Security and Malware scan also run daily at 19:17 UTC. - **OpenSSF Scorecard** rates the repository's security practices on every change to `main` and weekly, and the README badge shows the result. Some of its checks do not fit this project. A single maintainer cannot - have a second person approve every change. The class module and - workbooks are built locally rather than by CI, so a release carries - `vX.Y.Z-sha256.txt` and the security report rather than a build - provenance signature. Fuzzing does not apply: ROneCOne is VBA, which runs - only inside Office. + have a second person approve every change. Fuzzing does not apply: + ROneCOne is VBA, which runs only inside Office. Signed-Releases rises as + releases carry the provenance bundle; it counts the last five. ## Accepted findings @@ -165,22 +163,41 @@ Malware scan pass; a third-party major version waits for review. ## Releases -The class module and demo workbooks are built locally, and the GitHub -release carries `ROneCOne.cls`, every demo workbook, and -`vX.Y.Z-sha256.txt`, the SHA-256 of each of those files. - -Publishing the release starts `.github/workflows/release-security.yml`. It -downloads the release's `.cls` and `.xlsm` files, scans them with olevba -and mraptor against the baseline at the release tag, and attaches -`vX.Y.Z-security-report.md`: what olevba and mraptor find in every file, -their hashes, and whether the results match the reviewed baseline. -Releases from 1.10.2 on carry it. Started by hand with a release's tag, -the workflow is a dry run and attaches nothing. +Pushing a `vX.Y.Z` tag runs the Publish workflow. It refuses a tag that +is not the version in `src/ROneCOne.cls`'s release header, and fails if +restamping the release headers would change a committed module. It writes +`ROneCOne.cls` from the tagged commit, with the CRLF line endings the +Visual Basic Editor imports, runs Security and Malware scan on that commit, +scans the built file with olevba and mraptor, and only when all of that +passes signs the build provenance and creates the GitHub release with: + +- `ROneCOne.cls`: the runtime. +- `vX.Y.Z-sha256.txt`: its SHA-256. +- `ROneCOne-X.Y.Z.sigstore.json`: the signed build provenance of both. +- `vX.Y.Z-security-report.md`: what olevba and mraptor find in + `ROneCOne.cls`, its hash, and whether the results match the reviewed + baseline at the tag. Releases from 1.10.2 on carry it. + +Releases no longer attach the demo workbooks. They are in the repository's +`demo/` folder, and every change to them is scanned as described above. +Releases up to 1.10.3 were built locally, carry no provenance, and also +attach the demo workbooks. + +Started by hand, Publish is a dry run: it builds, scans and assembles the +same files and keeps them as a workflow artifact instead of releasing +anything. ### Verifying a release -Compare a download's SHA-256 with `vX.Y.Z-sha256.txt` or the security -report: +To check that a file was built by this repository's Publish workflow from +a tagged commit: + +```bash +gh attestation verify ROneCOne.cls --repo WilliamSmithEdward/ROneCOne +``` + +Without the GitHub CLI, compare a download's SHA-256 with +`vX.Y.Z-sha256.txt` or the security report: ```powershell Get-FileHash .\ROneCOne.cls -Algorithm SHA256 diff --git a/docs/README.md b/docs/README.md index 627e0d7..3e9dd33 100644 --- a/docs/README.md +++ b/docs/README.md @@ -72,6 +72,7 @@ syntax. Each technical document links back to its user guide. ## Release history - [Changelog](../CHANGELOG.md) records every released change. -- [Release notes](releases/) archive the published notes and SHA-256 checksums for each version. +- [Release notes](releases/) archive the published notes for each version, and the SHA-256 + checksums up to 1.10.2. Later checksums are on each GitHub release. [Back to the project overview](../README.md) diff --git a/docs/development.md b/docs/development.md index 58bdcb1..10d9cb2 100644 --- a/docs/development.md +++ b/docs/development.md @@ -180,9 +180,9 @@ runs each of those failures on small modules written for it. Update the baseline only after reviewing what it records, in the same commit as the change that caused it. Repackaging the demo workbooks can change their findings, so a release runs the scan once the workbooks are final. The Security workflow runs the check on every push and pull request. -When a release is published, the release-security workflow downloads its assets, writes -`-security-report.md` with each asset's olevba findings, mraptor verdict, SHA-256 hash, and -comparison with the baseline at the tag, and attaches the report to the release. +When a `vX.Y.Z` tag is pushed, the Publish workflow builds `ROneCOne.cls` from the tagged commit, +writes `vX.Y.Z-security-report.md` with its olevba findings, mraptor verdict, SHA-256 hash, and +comparison with the baseline at the tag, and attaches the report to the release it creates. The Malware scan workflow runs `tools/malware_scan.py`, once per scanner with `--only`, using ClamAV's official signatures and a pinned YARA Forge Core package through YARA-X. The scan covers the shipped runtime, every demo diff --git a/docs/user-guide/README.md b/docs/user-guide/README.md index d58c10f..db4f7db 100644 --- a/docs/user-guide/README.md +++ b/docs/user-guide/README.md @@ -26,8 +26,7 @@ class module, and copy the smallest example that solves your problem. ## Fastest route to the first result -1. Download the latest demo workbooks from the - [release page](https://github.com/WilliamSmithEdward/ROneCOne/releases/latest). +1. Download the demo workbooks from the repository's [`demo/` folder](../../demo/). 2. Open the Collections demo and run its main macro. 3. Follow [Getting started](getting-started.md) to import `ROneCOne.cls` into a copy of your own workbook. diff --git a/docs/user-guide/excel-tables.md b/docs/user-guide/excel-tables.md index 2adab1c..0a04a37 100644 --- a/docs/user-guide/excel-tables.md +++ b/docs/user-guide/excel-tables.md @@ -174,8 +174,8 @@ so. ## See it running The Excel Tables demo workbook builds a real table and works through all of -the above, live, with the result next to what was expected. Download it from -the [release page](https://github.com/WilliamSmithEdward/ROneCOne/releases/latest). +the above, live, with the result next to what was expected. Download +`ROneCOne_ListObject_Demo.xlsm` from the repository's [`demo/` folder](../../demo/). ## Related diff --git a/docs/user-guide/getting-started.md b/docs/user-guide/getting-started.md index af14626..b38227f 100644 --- a/docs/user-guide/getting-started.md +++ b/docs/user-guide/getting-started.md @@ -5,10 +5,10 @@ installation is one class import. ## Option 1: see it working first -1. Open the [latest release](https://github.com/WilliamSmithEdward/ROneCOne/releases/latest). +1. Open the repository's [`demo/` folder](../../demo/). 2. Download the demo workbook for the capability you want to explore. 3. Save it to a trusted local folder and open it in Excel. -4. Enable macros only after confirming that the file came from the ROneCOne release. +4. Enable macros only after confirming that the file came from the ROneCOne repository. 5. Press `Alt+F8`, select the demo macro, and choose **Run**. | Workbook | Macro |