Skip to content

docs: sync certified-assets docs and make static-site the primary frontend path - #397

Merged
marc0olo merged 17 commits into
mainfrom
docs/frontends-static-site
Sep 17, 2026
Merged

marc0olo merged 17 commits into
mainfrom
docs/frontends-static-site

Conversation

@marc0olo

@marc0olo marc0olo commented Sep 16, 2026

Copy link
Copy Markdown
Member

Summary

Publishes the eight certified-assets user-doc pages as a synced tree under Frontends, and makes a static site the default path our own pages teach. They taught @dfinity/asset-canister while the templates, the icp-cli docs site, and this site all deploy @dfinity/static-site, and every configuration detail differs between the two.

Closes #396

Decisions worth your judgment

  • Fetch-based sync, not a submodule. The build resolves no file from that repo, only markdown links, so the pin is a ref in .sources/upstream.json under a new synced group and scripts/sync-static-site.mjs fetches the pages at it. .github/workflows/sync-static-site.yml opens a bump PR weekly for any release the pin does not contain: normally a content diff, and when a release touched no page, a pin-only PR whose whole diff is source_ref, which is what moves the pin off a commit and onto a tag.
  • The pin is a commit, not the v0.3.3 tag. Every commit past that release is docs-only, and the tag predates the frontmatter contract the sync needs, so the tag cannot be synced at all. .sources/VERSIONS forbids pinning past a release; the reasoning for why this is not that case is in the entry's $comment, and the pin returns to tags when a release contains the pages.
  • The synced tree is not exempt from the validator. Only docs/languages/motoko is. The sync normalizes and rewrites so the pages pass the same checks as a hand-written page, and exits non-zero instead of publishing pages that do not.
  • validate.js now resolves link fragments repo-wide, because our pages deep-link headings upstream can rename. Seven links were already broken, three across pages and four within one. Anchors use github-slugger, the library Starlight itself uses, because a local approximation disagreed on 21 of the repo's 2995 headings. It was already present as a transitive dependency and is now declared, with package-lock.json hand-edited to a one-line diff (npm install on macOS prunes this lockfile).
  • The sync rewrites one command in upstream content. A synced page published dfx, which this repo bans outright, so the sync translates that exact command to its verified icp equivalent, reports it, and refuses to publish any other dfx shape rather than guessing. Everything else upstream writes is published as written.
  • The legacy canister stays recommended for one case. Proposal-gated frontend updates for an SNS do not exist on certified-assets (verified against its .did at the pinned ref: no proposal-gated commit, no staged-batch evidence, authorization is controllers plus a flat syncer set). asset-canister.md gains "When to stay on this canister" and the governance guide says which canister its workflow requires.
  • Naming rules are now in AGENTS.md: which of the three names ("frontend canister", "static site", "certified-assets") belongs where, that links go to "Deploy a static site" except for the canister itself which links to its repo, and that a link never sits on part of a compound term.

Reviewing it

The commits are scoped so they can be read in order. docs/guides/frontends/static-site/ is generated: review the diff, not the pages, and never hand-edit them. The rest is our own prose, with the naming pass across concepts/, getting-started/ and the guides being mechanical.

Five Copilot rounds have already swept the mechanical layer, and every finding is answered in the threads and in the feedback-addressed comments. What none of them reached, and what is worth your judgment: whether the dfx rewrite should exist at all, whether the naming pass belongs in this PR or its own, whether the Frontends structure reads right to a developer arriving cold, and whether the SNS exception is the right thing to tell people.

Verified

  • npm run build and npm run validate --all pass on all 218 pages, including the synced tree; build warnings 276 → 275
  • Every link into the tree resolves, and every anchor in the repo now resolves
  • Sidebar renders as intended under a group label we own; llms.txt derives both sections
  • CLI flags checked against docs/reference/cli.md at v1.5.0; template recipes against dfinity/icp-cli-templates; the certified-assets interface against its .did at the pinned ref
  • The sync was tested against a pre-contract ref: one legible error, exit 1, tree untouched

Open question

Whether a synced tree needs the upstream license alongside it and an entry in NOTICE. dfinity/certified-assets is Apache-2.0, the same license and holder as this repo, and neither existing synced tree has an entry, including docs/languages/motoko/, which comes from another org. So either none of the three needs one or all three do; adding one only here would be the least coherent option.

Out of scope

#279 (stale <!-- Upstream: --> comments) is unblocked now that #355 is closed, but it is a sweep over 108 unrelated files; this sync writes provenance as frontmatter and adds no new instance. No upstream issue was filed for proposal-gated updates: the topic is already under consideration there, and the docs drop the exception if it ships.

Publishes the eight user-doc pages of dfinity/certified-assets as a synced
tree under guides/frontends/static-site/, with the repo staying the single
source of truth.

Fetch-based rather than a submodule: the build resolves no file from that
repo, only markdown links, so the pin is a ref in .sources/upstream.json
under a new `synced` group and the pages are fetched at that ref.

The script exits non-zero instead of publishing something broken: missing
frontmatter, an absolute link to this site it cannot map, a relative link
that does not resolve, or a banned character that survived normalization.
The tree is deliberately not exempt from the validator.

Pinned to a commit rather than the v0.3.3 tag: the only two commits past
that release are the upstream docs PRs, and the tag predates the frontmatter
contract this sync requires.

Refs #396
Our Frontends pages taught @dfinity/asset-canister as the default while the
templates, the icp-cli docs site, and this site all deploy @dfinity/static-site.
Every configuration detail differs between the two, so the pages were not
just out of date but wrong for anyone following them.

- certification.md: every raw-access and automatic-behavior claim named one
  canister's answer as the platform's. Each claim now says which recipe it
  applies to. allow_raw_access stays documented; it is still correct for the
  asset canister, and certified-assets has no equivalent by design
  (certified-assets#123), so the static-site case is the gateway trust model
  instead.
- asset-canister.md: reframed as the legacy path, moved last in the section,
  and given a migration section covering the reinstall requirement, the
  configuration mapping, and why per-file writes cannot come back.
- frameworks.md, custom-domains.md, troubleshooting.md, internet-identity.mdx:
  _headers and _redirects first, .ic-assets.json5 kept as the legacy path. The
  II alternative-origins and app-metadata recipes need hand-written _headers
  rules, since static-site sets no headers at all.
- application-canisters.md: adds the certified-assets interface, verified
  against its .did, and labels the asset canister section legacy.
- project-structure.mdx: the templates ship static-site, and the recipe table
  was missing it.

Refs #396
@marc0olo
marc0olo requested review from a team as code owners September 16, 2026 10:34
The sidebar showed "static-site", which is a recipe name rather than
anything a reader is looking for, because an autogenerated group takes its
label from the directory name. Frontends now lists its pages explicitly so
the group can be called "Hosting a static site"; the pages inside it stay
autogenerated, so a page added upstream still reaches the sidebar through
the sync alone.

One vocabulary, applied across the section: prose, headings, and the
sidebar name the goal ("a static site"), the recipe name appears where it
is typed (`@dfinity/static-site`), and the canister name appears only where
its identity matters (its Candid interface, state-hash verification, and
contrasting it with the legacy canister). Labels like
"static-site (certified-assets)" made a reader learn three names for one
thing; the rule is now written down in AGENTS.md.

Frontend frameworks now precedes custom domains. The frontmatter order
values stay in step with the explicit array because llms.txt still sorts
pages within a section by them.

Refs #396
The asset canister page linked only the word "canister" in its opening
sentence, splitting the very term the page is about. Unlinked: the page has
nowhere good to point for that name, since the canister's own source is
poorly documented and its recipe is a different thing with a different name.

Five links elsewhere still sent readers to that page for tasks that are not
legacy: the architecture comparison table's frontend-hosting row, three
next-steps entries, and a pointer to an agent setup example that has nothing
to do with which canister serves the files. They now point at the static site
guide, or at frameworks.md for the agent example. The troubleshooting entry
splits in two, since `.ic-assets.json5` genuinely belongs to the legacy page.

AGENTS.md gains the rule behind all of this: link the whole term, match the
link to what the sentence is about, and keep recipe names pointing at recipe
sources rather than at canister behavior.

Refs #396
certified-assets#128 lands the retitle asked for in #127: the entry page is
now "Deploy a static site", and its description no longer names the recipe
and the canister in one sentence. Upstream also recorded the naming
convention in its own rules file, and replaced the remaining icp0.io
hostnames with icp.net.

The sidebar entry under "Hosting a static site" therefore reads "Deploy a
static site" with nothing left to override on our side, and one of our own
Next steps entries that still used the old title as its link text now names
the goal like the rest.

The pin stays a commit: every commit past v0.3.3 is docs-only, so there is
still no release that contains these pages.

The bare cli.internetcomputer.org links the new lead introduces are left as
upstream wrote them. That root meta-refreshes to the newest version, so it
always lands on current docs, and AGENTS.md now records the exception so the
next sync does not try to "fix" it.

Refs #396
Three pages still sent a first-time reader to the legacy canister as the way
to serve a frontend, which is the opposite of what the rest of this branch
establishes:

- guides/index.md, the landing page for every guide section, linked Frontends
  straight to asset-canister.md
- choose-your-path.md said "Start with: Asset canister" for the web app path
- calling-from-clients.md pointed there for "deploying the frontend that makes
  these calls"

All three now point at the static site guide. Every remaining link to the
legacy page is about the legacy path itself, and one of them was labeled
"Asset canister guide" rather than naming it as legacy.

No section landing page was added: guides/index.md already orients readers a
level up, no other guides/ section has one, and a Frontends-only hub would be
a third place to keep the same description in sync.

Refs #396
…rammatic uploads as a pattern

dfinity/examples removed hosting/photo-storage, because uploading assets from
application code is no longer recommended and certified-assets does not
support it at all. The link is gone, and both places that document
AssetManager now say up front that it is specific to the legacy canister and
not a pattern to adopt: an app with user-generated content keeps that content
in a canister of its own.

The API stays documented for projects already doing it.

Refs #396
Stress-testing the static-site sync turned up three weaknesses.

**Anchors were checked by nothing.** Our pages deep-link five headings inside
the synced tree, and upstream can rename a heading whenever it likes, which
would drop readers at the top of a long page with a clean build and a clean
validator. `validate.js` now resolves the fragment of every relative `.md`
link, repo-wide rather than only for the synced tree, since the same rot
happens whenever anyone edits a heading. Three links were already broken:
`#ic-assets-json5` (the real id is `ic-assetsjson5`), `cycle-costs.md#vetkd`,
and `#reject-anonymous-callers`.

Anchors are computed with `github-slugger`, the library Starlight itself uses,
rather than a local approximation: comparing the two over all 2995 headings in
docs/ showed 21 disagreements, on headings containing arrow, ampersand, and
`/*`. It was already in the tree as a transitive dependency and is now
declared, with the lockfile edited by hand to keep the entry a one-line diff.

**A failing sync could leave a half-updated tree.** Pages were written inside
the fetch loop, so a page violating the contract left earlier pages at the new
ref and later ones at the old, a state that still builds and still validates.
Every page is now transformed before any page is written.

**A contract violation printed a stack trace.** Fail-loud only helps if the
message is legible, so the failure paths now report one line and exit 1.

Verified against a pre-contract ref (`--ref v0.3.3`, where no page has
frontmatter): one line naming the file and the contract, exit 1, tree
untouched. A normal run is unchanged and idempotent.

Refs #396
"Asset canister" was still the generic name for the role across ten pages,
which now points readers at the legacy path. Where a statement is true of
both canisters, it names the role instead: the bullet introducing the two
canisters an app starts with, the architecture comparison table (now
"Frontend canister (certified responses)", matching "Backend canister
(Wasm)"), the `ic_env` cookie in the client and Internet Identity guides, and
the certification sentences in the concepts pages.

Two were stale recommendations rather than naming: certified-variables told
readers to use the asset canister for frontend assets, and service
discoverability documented only the legacy `.well-known` configuration, which
is also the fiddlier one. On a static site that file needs no configuration at
all.

Mentions that are genuinely about the legacy canister are untouched: the
`.ic-assets.json5` references, and the SNS workflow in the governance guides,
which uses that canister's `Prepare`/`Commit` roles and
`propose_commit_batch`.

Refs #396
Naming: "frontend canister" is the role and stays the default in prose about
what the component does; "static site" is the task and is what link text uses;
"certified-assets" names the canister only where its identity matters. Whatever
noun a sentence needs, the link now goes to "Deploy a static site", the only
page that documents how to get one. Added on first mention where a reader
plausibly needs it, not on repeats, asides, or code comments. The rule is in
AGENTS.md so the next page does not have to re-decide.

The SNS exception: proposal-gated frontend updates are not possible on
certified-assets, so telling readers to migrate everything was wrong. Verified
against certified-assets.did at the pinned ref: its twenty methods include no
proposal-gated commit and no staged-batch evidence, and its authorization is
controllers plus a flat set of syncers who can all sync at will. An SNS could
hold the controller with nothing for token holders to approve.

So the legacy page now carries a "When to stay on this canister" section, its
caution names the exception rather than saying everyone should migrate, the
SNS asset-update workflow says which canister it requires, and the reference
page records where the capability lives.

Refs #396
When prose names certified-assets, it means the canister itself: its source,
interface, and releases. That lives in dfinity/certified-assets, so the name
links there, once per section rather than per mention, since both pages are
entered through deep links.

Recorded in AGENTS.md alongside the naming rule, including why it does not
contradict "link internally when an internal page exists": our pages remain
the target for the canister's behavior and its Candid surface.

No issue filed upstream for proposal-gated updates; the topic is already being
considered there, and the docs will drop the exception if it ships.

Refs #396
A new page under guides/frontends/ needs a sidebar.mjs entry now that the
group is explicit, and sidebar.order alone will not place it. The reason was
only in a code comment, which is not where someone adding a page looks.

Refs #396

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved workflow, synchronization, validation, and documentation issues remain.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR syncs certified-assets documentation and makes static-site the primary frontend path while retaining the legacy asset-canister path for SNS-governed updates.

Changes:

  • Adds upstream pinning, synchronization, validation, provenance, and weekly automation.
  • Adds eight static-site documentation pages and reorganizes frontend navigation.
  • Updates frontend, security, authentication, governance, testing, and getting-started guidance.
  • Adds repository-wide anchor validation and naming rules.
File summaries
File Reviewed changes
sidebar.mjs Adds static-site navigation grouping.
scripts/validate.js Adds repository-wide anchor validation.
scripts/sync-static-site.mjs Fetches, transforms, and validates upstream pages.
package.json Adds sync tooling and slugger dependency.
package-lock.json Locks dependency metadata.
docs/references/application-canisters.md Documents frontend canister options.
docs/guides/testing/strategies.md Updates frontend testing terminology.
docs/guides/security/data-integrity-and-authenticity.md Updates frontend integrity guidance.
docs/guides/security/canister-control.md Generalizes frontend canister terminology.
docs/guides/index.md Makes static-site the frontend entry point.
docs/guides/governance/managing.md Documents the legacy governance workflow.
docs/guides/frontends/static-site/verifying-contents.md Adds state-hash verification guidance.
docs/guides/frontends/static-site/site-files.md Documents uploaded and skipped files.
docs/guides/frontends/static-site/routing.md Documents routing and clean URLs.
docs/guides/frontends/static-site/redirects.md Documents redirect and rewrite rules.
docs/guides/frontends/static-site/overview.md Adds the static-site deployment guide.
docs/guides/frontends/static-site/how-it-works.md Explains certification and serving behavior.
docs/guides/frontends/static-site/headers.md Documents custom headers.
docs/guides/frontends/static-site/access-protection.md Documents access protection.
docs/guides/frontends/service-discoverability.md Updates service discovery guidance.
docs/guides/frontends/frameworks.md Teaches static-site framework deployment.
docs/guides/frontends/custom-domains.md Updates custom-domain guidance.
docs/guides/frontends/certification.md Contrasts certification behavior.
docs/guides/frontends/asset-canister.md Reframes the asset canister as a legacy option.
docs/guides/canister-management/troubleshooting.md Updates frontend troubleshooting.
docs/guides/canister-calls/calling-from-clients.md Updates frontend hosting examples.
docs/guides/backends/certified-variables.md Updates frontend certification links.
docs/guides/authentication/internet-identity.mdx Adds static-site header configuration.
docs/getting-started/quickstart.md Uses static-site in the quickstart.
docs/getting-started/project-structure.mdx Updates project recipe examples.
docs/getting-started/choose-your-path.md Makes static-site the recommended path.
docs/getting-started/app-architecture.md Updates frontend architecture guidance.
docs/developer-tools/index.md Updates recipe terminology.
docs/concepts/vetkeys.md Corrects a cycle-cost anchor.
docs/concepts/security.md Corrects an access-management anchor.
docs/concepts/edge-infrastructure.md Updates frontend certification references.
docs/concepts/certified-data.md Updates certified-assets terminology.
AGENTS.md Documents synced trees and naming rules.
.sources/upstream.json Adds the certified-assets sync pin.
.github/workflows/sync-static-site.yml Automates documentation synchronization.
.agents/upstream-tracking.md Documents synced-tree maintenance.
Review details

Suppressed comments (7)

.sources/upstream.json:63

  • The automated workflow updates only entry.pinned. After the first release bump, this $comment will still say the current pin is a commit past the latest release, so the source manifest will carry false operational guidance. Make this wording historical or update the comment as part of the bump.
        "Pinned to a commit, not a release tag, and past the latest release.",
        ".sources/VERSIONS forbids pinning past a release so that docs cannot",
        "describe unshipped behavior; this is the case that rule is not aimed at.",

AGENTS.md:220

  • After adding the synced and reference sections, this says upstream repos are tracked three ways, but this document now defines four: vendored, synced, watched, and reference (also recorded as four in .sources/upstream.json). This misdirects contributors about which source procedure applies. Change “three” to “four”.
Upstream repos are tracked three ways. Which one decides where you read from.

docs/guides/frontends/static-site/verifying-contents.md:54

  • This example is presented as command output but is not a verifiable output: the hash contains an ellipsis while the comment claims 64 hexadecimal characters. The documentation rules prohibit invented output, so replace it with output copied from an upstream fixture or mark it explicitly as needing verification.
   # 8150a65e854b9bbb…  (64 hex chars, and nothing else)

docs/guides/frontends/static-site/verifying-contents.md:69

  • This second example also presents invented command output: the blob value is truncated with an ellipsis, so readers cannot use it to reproduce or compare the result. Replace it with output copied from an upstream fixture or mark it explicitly as needing verification.
   # (blob "\81\50\a6\5e…")

docs/guides/index.md:18

  • The generic 404 page still directs users to /guides/frontends/asset-canister/ as the Frontends entry point, so this PR leaves a prominent navigation path on the legacy recipe despite changing the guide index to static-site. Update docs/404.mdx to point at the static-site overview and use the new description.
    scripts/sync-static-site.mjs:160
  • brokenLinks discards the fragment and only checks the file path. Because validate.js skips synced files, a future upstream heading rename can leave these generated deep links broken without failing the sync. Validate fragments against the generated target, using the same slugger, before accepting the tree.
    scripts/sync-static-site.mjs:94
  • This only distinguishes fenced blocks, so fn also receives inline code, link destinations, and frontmatter. The promised prose-only normalization can therefore rewrite a future identifier or URL, such as tamper-proof, and break it. Protect those regions or use a Markdown-aware transform.
  • Files reviewed: 40/41 changed files
  • Comments generated: 7
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/workflows/sync-static-site.yml Outdated
Comment thread .sources/upstream.json
Comment thread docs/guides/frontends/static-site/verifying-contents.md Outdated
Comment thread scripts/sync-static-site.mjs
Comment thread scripts/validate.js
Comment thread .github/workflows/sync-static-site.yml Outdated
Comment thread AGENTS.md Outdated
Fourteen findings, seven posted and seven suppressed. The substantive ones:

**A synced page published a `dfx` command.** dfx is banned here, and the ban
was never enforced anywhere, so it shipped. `icp canister call` takes a
principal and `-e` selects the network (checked against cli.md at v1.5.0), so
the sync now rewrites that exact shape and reports it, and refuses to publish
any other `dfx` occurrence rather than guessing at a translation.

**Same-page anchors were never checked.** The fragment check skipped `#`-only
hrefs, so `(#section)` links could rot freely. Four were already broken, in
the glossary and the Candid spec.

**The atomicity guarantee did not hold.** Link and banned-character checks ran
after writing, so a failure left the tree modified while claiming otherwise.
All checks now run against the in-memory pages, and those checks include link
fragments, resolved against the pages about to be written. The anchor logic is
now one module shared with the validator instead of two implementations.

**Prose normalization could reach into code.** It skipped fenced blocks but not
inline code, link destinations, or frontmatter, so a future `tamper-proof`
identifier or URL could have been rewritten. Unit-tested against all four.

**The workflow interpolated upstream filenames into shell source.** They now
arrive as environment values, so nothing upstream writes can become script.

Also: the manifest `$comment` described the current pin, which the workflow
rewrites, so it stated a rule instead; `AGENTS.md` said upstream repos are
tracked three ways when it defines four, and its own table used the mixed
label the naming rule forbids; and the 404 page still sent readers to the
legacy canister as the Frontends entry point.

Not taken: extending the validator to ban `dfx` repo-wide. The SNS guides
reference it deliberately, with a note that `icp-cli` has no equivalent for
the `dfx sns` extension, so that needs an exemption mechanism and its own
change.

Refs #396
@marc0olo

Copy link
Copy Markdown
Member Author

Copilot's review had seven posted comments and seven suppressed ones. The posted ones are answered and resolved in their threads. The suppressed ones, since they have none:

Fixed in c081394

  • .sources/upstream.json:63 (the $comment will go stale, because the workflow rewrites only pinned): right, and the fix is that it no longer describes the current pin. It states the rule instead, when a commit pin is permitted and what to check before using one, plus one historical sentence about the bootstrap.
  • AGENTS.md:220 ("three ways" vs four groups): correct, now four.
  • docs/guides/index.md:18 (the 404 page still points Frontends at the legacy recipe): a genuine find, and the last entry point I had missed. docs/404.mdx now points at the static site overview with the matching description.
  • scripts/sync-static-site.mjs:160 (brokenLinks discards the fragment): fixed, and it now resolves sibling targets against the pages about to be written, so it runs before anything is on disk. One correction to the premise: the synced tree is deliberately not in the validator's SYNCED allowlist, so validate --all was already checking these fragments after the anchor work. The sync now checks them too, so a manual run is self-sufficient, and the anchor logic is one shared module rather than two implementations.
  • scripts/sync-static-site.mjs:94 (prose rules also see inline code, link destinations, and frontmatter): fixed, with a unit test over all four regions. The em-dash rule also left a double space behind, which the same change corrects.

Not fixed, deliberately

  • verifying-contents.md:54 and :69 (the elided state-hash output and the truncated blob): these are upstream's pages, generated by the sync, and hand-editing them is the one thing the tree must never have done to it. On the substance I also disagree that this is invented output: both are deliberate elisions of a value that differs per build, and the first explicitly tells the reader what the real output looks like ("64 hex chars, and nothing else"). A hash that varies by deployment cannot be copied from a fixture. The narrow fair point is that "64 hex chars" sits next to a value showing sixteen, which is worth raising upstream as prose, not worth overriding here.

Not taken

  • Extending the validator to ban dfx repo-wide, which is where the posted finding about the synced page pointed. I tried it: it surfaces 29 occurrences, 23 of them in the SNS guides, which reference dfx sns deliberately and say so ("No icp-cli equivalent exists yet"). Enforcing the ban needs an exemption mechanism and belongs in its own change.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved moderate findings affect synchronization, validation, workflow behavior, and source metadata.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (10)

Previously missed (1) — in code that hasn't changed since the last review.

docs/guides/frontends/static-site/verifying-contents.md:55

  • This shows a synthetic, truncated hash as command output, but state-hash depends on the reader's complete dist tree and the preceding steps provide no fixture that could produce this value. Readers cannot reproduce 8150a65e..., so this should be removed or replaced with output copied from a real upstream fixture before regenerating the synced page.

.github/workflows/sync-static-site.yml:64

  • When a new release is a descendant of the pinned docs-only commit but has no additional changes under docs/, this empty-CHANGED branch skips the update. The pin then remains at the commit instead of moving back to the release tag, contrary to the documented policy and leaving future syncs on a non-release ref. Treat a descendant tag with an empty docs diff as needed=true so the manifest pin is advanced (the regenerated pages can otherwise be unchanged).
          CHANGED=$(git -C /tmp/certified-assets diff --name-only "${PIN}..${TAG}" -- docs/)
          if [ -z "$CHANGED" ]; then
            echo "No docs/ changes between $PIN and $TAG. Skipping."
            echo "needed=false" >> $GITHUB_OUTPUT
          else

.sources/upstream.json:58

  • The manifest pins 65c0f32, but the PR description and this entry's rationale still say the sync was bootstrapped at d9cb7df. Since the generated pages also record 65c0f32, the reproducibility record is inconsistent; update the description and $comment to the actual pin, or reset the pin and generated provenance to the stated bootstrap ref.
      "repo": "dfinity/certified-assets",
      "pinned": "65c0f32",
      "source": "docs/",
      "target": "docs/guides/frontends/static-site/",
      "script": "scripts/sync-static-site.mjs",

AGENTS.md:249

  • The new synced-tree model is not reflected in CONTRIBUTING.md: it still says upstream repos are tracked in two ways and omits docs/guides/frontends/static-site/ from the synced-content list. Contributors following the repository-level guide will therefore receive contradictory instructions about where these pages come from and whether they may edit them. Update the contributor guide in this PR.
| Topic | Repo | Synced tree |
|-------|------|-------------|
| certified-assets canister behavior | `dfinity/certified-assets` | `docs/guides/frontends/static-site/` |

docs/references/application-canisters.md:12

  • This sentence says there is “no uncertified serving path,” but the static-site guidance documents <canister-id>.raw.icp.net as a gateway that forwards the certified response without checking its proof. The canister attaches a certificate, but visitors on that raw host do not receive a verified response; qualify the claim with the gateway distinction so readers do not treat raw URLs as integrity-protected.
    scripts/sync-static-site.mjs:140
  • The lookup uses the full canonical URL as its key, so a mapped page URL with a fragment (for example, .../guides/frontends/custom-domains#...) is treated as unmapped even though this table is documented as accepting path#fragment. The next upstream link of that form will fail the sync; split the fragment before the lookup and append it to the mapped relative target.
    scripts/sync-static-site.mjs:67
  • This pattern does not match the exact retired URL https://internetcomputer.org/docs because it requires another slash and a nonempty path after the optional /docs. That URL would survive the rewrite and the synced-tree validators would publish the retired link, despite the comment saying the retired /docs form is supported. Add an explicit end-of-URL case while continuing to exclude the bare domain.
    scripts/sync-static-site.mjs:186
  • When upstream removes a page, it is absent from prepared, but this fallback still accepts the stale copy currently on disk. The later cleanup then deletes that copy, leaving a dangling link that the pre-write validation incorrectly approved. Treat paths inside TARGET_DIR as valid only when their relative filename exists in prepared; use the on-disk fallback only for pages outside the synced tree.
    scripts/sync-static-site.mjs:335
  • The final sync phase writes directly into the target and then deletes stale files. An interruption or I/O error after one write can therefore leave a mixed checkout containing pages from two refs; a local rerun or subsequent tooling can observe that partial tree. Stage the complete output in a temporary directory and replace the target only after every write succeeds.
    scripts/validate.js:93
  • The new repo-wide anchor check still treats any href beginning with http as an external URL. That skips relative files such as http-gateway-protocol-spec.md, and a future http-gateway-protocol-spec.md#... link would bypass fragment validation entirely. Restrict this condition to actual URL schemes so filenames beginning with http are checked.
  • Files reviewed: 44/45 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread scripts/sync-static-site.mjs Outdated
Comment thread .sources/upstream.json
Twelve findings, two posted and ten suppressed. The ones that could produce
wrong output:

**The dfx rewrite could publish a broken command.** `--network ic` was
optional in the pattern, so `dfx canister call foo bar --network local` would
have had its prefix rewritten and kept its flag, yielding `icp ... -e ic
--network local`, which conflicts, and the dfx check could no longer catch it
because the word was gone. The pattern now matches one complete supported
command anchored to the line, and every other shape falls through to the
failure path. Unit-tested over four shapes.

**The validator skipped relative files beginning with "http".** It tested
`href.startsWith('http')`, so `http-gateway-protocol-spec.md`, which exists
and is linked, was treated as an external URL and checked for nothing. Now
matched as a URL scheme.

**A page removed upstream could leave an approved dangling link.** The
pre-write link check fell back to the copy on disk, which the swap then
deletes. Inside the tree, `prepared` is now the only truth.

**The retired `internetcomputer.org/docs` URL with no path escaped the
rewrite** and would have been published as-is.

**A mapped page reached with a fragment counted as unmapped**, so the next
upstream link of that shape would have failed the sync even though the map is
documented as accepting `path#fragment`. The fragment now carries over.

**A release with no docs changes stranded the pin on a commit.** The workflow
skipped when `docs/` was unchanged, so the pin would never have moved back onto
a release tag, which is the whole plan for it. It now opens the PR either way
and says which of the two it is.

**The write was not atomic.** Pages are staged outside `docs/` (a staging
directory left inside it would be built as content) and swapped in by rename,
which also subsumes deleting pages that upstream removed.

Also: the manifest `$comment` read as a description of the current pin rather
than the policy; the `static-site` watched entry still said these pages were
"not yet documented"; `CONTRIBUTING.md` said upstream repos are tracked two
ways and omitted the tree from its synced list; and the reference page claimed
there is "no uncertified serving path" without the gateway distinction that
makes a raw URL unverified for the visitor.

Left for upstream: the elided `state-hash` output, which the maintainer is
handling in certified-assets.

Refs #396
@marc0olo

Copy link
Copy Markdown
Member Author

Second Copilot review: two posted comments, answered and resolved in their threads, and ten suppressed. The suppressed ones, all in 38366ba except where noted:

Fixed, and these could have produced wrong output

  • scripts/validate.js:93 (the anchor check skips anything starting with http): real, and it bites today. docs/references/http-gateway-protocol-spec.md exists and is linked relatively, so it was treated as an external URL and checked for nothing, existence included. Now matched as a URL scheme.
  • scripts/sync-static-site.mjs:186 (the on-disk fallback approves a link to a page upstream removed): right, and the swap would then delete the target. Inside the tree, the pages about to be written are now the only truth.
  • scripts/sync-static-site.mjs:67 (the retired internetcomputer.org/docs with no trailing path escapes the pattern): confirmed by test, fixed, and the bare domain still does not match, since upstream links it legitimately.
  • scripts/sync-static-site.mjs:140 (a mapped page reached with a fragment counts as unmapped): fixed, the fragment carries over to the relative target, and a map entry may still pin a specific section.
  • .github/workflows/sync-static-site.yml:64 (a release with no docs/ changes strands the pin on a commit): the best catch of the round, because it defeats the plan for the pin rather than breaking anything visibly. The workflow now opens the PR either way and the body says whether pages changed or only source_ref did.
  • scripts/sync-static-site.mjs:335 (an interrupted write leaves a mixed tree): pages are staged and swapped in by rename. One correction worth recording: staging inside docs/ would have been worse than the problem, since a directory left by an interrupted run would be built as content pages, so the staging area lives outside the content root and is cleaned up on both paths.

Fixed, documentation consistency

  • .sources/upstream.json:58 ($comment vs the actual pin): it now reads as policy rather than as a description of the current ref, which is what made it drift in the first place.
  • .sources/upstream.json:59, posted: the static-site watched entry still said these pages were "not yet documented".
  • AGENTS.md:249CONTRIBUTING.md: correct, and the contributor-facing guide was the one that mattered. It said upstream repos are tracked two ways and omitted the tree from its synced list; both fixed.
  • docs/references/application-canisters.md:12 ("no uncertified serving path"): fair. The canister has none, but a visitor on a raw host still gets an unverified response, so the sentence now names the gateway distinction and links to it.

Left for upstream

  • verifying-contents.md:55 (the elided state-hash output): the maintainer is taking this up in certified-assets directly, so nothing changes here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The review includes one critical workflow issue and several moderate documentation, synchronization, and anchor-validation issues.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (9)

.github/workflows/sync-static-site.yml:61

  • The PR description says this workflow opens a bump PR only when docs/ changes, but this branch deliberately sets needed=true and opens a pin-only PR when a release contains no documentation changes. Reconcile the stated contract with the implementation, either by skipping that PR or documenting the pin-only release PR behavior.
          # A release that ships canister changes without touching docs/ leaves
          # the synced pages byte-identical, so there is no content to review.
          # The pin still has to move: it is allowed to sit on a commit only
          # while no release carries the pages, and skipping here would strand
          # it on that commit for good. So the PR is opened either way, and the
          # body says which of the two it is.

docs/404.mdx:16

  • This newly updated internal link uses a root-relative URL and a trailing slash, but the repository's linking rule requires relative .md links. Use guides/frontends/static-site/overview.md so the validator and source links can resolve the target consistently.
- **[Frontends](/guides/frontends/static-site/overview/)**: Host a static site, integrate frameworks, configure custom domains, and certify responses.

docs/concepts/certified-data.md:45

  • This now links the generic certified-assets explanation to the static-site canister, but the claim that the HTTP gateway verifies every browser response is too broad: the same canister also exposes raw hostnames where the gateway deliberately skips verification. Scope this sentence to a verifying hostname/gateway so readers do not mistake certification by the canister for verification of every URL.
- **Certified assets.** A [frontend canister](../guides/frontends/static-site/overview.md) uses certified variables to produce certified HTTP responses. When a browser fetches a page served by an ICP canister, the HTTP gateway verifies the certificate before serving the response, so the browser sees only content that was committed through consensus.

docs/getting-started/project-structure.mdx:121

  • This page now presents @dfinity/static-site as the frontend recipe, but the project-tree example above still labels frontend/canister.yaml as “Asset canister configuration.” Readers copying the documented layout are sent toward a legacy/nonmatching configuration file; update that tree to the static-site layout or explicitly mark it as legacy.
The frontend uses the `@dfinity/static-site` recipe, which builds the frontend app and uploads the output to a canister that serves it with response certification:

docs/guides/canister-management/troubleshooting.md:103

  • The static-site canister does add managed response headers such as Content-Type, ETag, certification headers, and ic_env; the synced static-site/headers.md documents these explicitly. The relevant troubleshooting distinction is that it adds no default security headers or CSP, so saying it sets "no headers of its own" is factually too broad. Narrow this sentence to default security headers/CSP.
**On a [static site](../frontends/static-site/overview.md)** the canister sets no headers of its own, so a CSP error means your own policy is missing or not matching. `_headers` patterns match the file that was served, not the URL the visitor asked for, so a rule written against a client route (`/dashboard/*`) matches nothing. Write it against the file instead (`/index.html` or `/*.html`), redeploy, and check the response:

docs/guides/frontends/certification.md:205

  • This sentence contradicts the raw-host behavior documented above: both recipes can be reached through a raw hostname, where the gateway forwards responses without checking the certificate. Limit the transparent-verification claim to requests sent through a verifying hostname/gateway; otherwise readers may treat a raw URL as authenticated.
For standard asset serving through either frontend recipe, verification is transparent: the boundary node verifies every response before forwarding it to the browser, and you do not need any JavaScript verification code.

docs/guides/security/data-integrity-and-authenticity.md:626

  • This recommendation now links to the static-site path, but the next bullet still tells readers to inspect http_request and reject raw-host requests. The synced static-site documentation explicitly says this canister cannot reliably distinguish or refuse raw hosts because Host is unauthenticated; following this guidance therefore asks static-site users to implement a capability the canister does not have. Scope the http_request rejection to custom HTTP/legacy asset-canister implementations and direct static-site users to a verifying gateway instead.
    scripts/lib/anchors.mjs:37
  • The heading-ID plugin supports both {#id} and {$id} forms, but this anchor index only recognizes the # form. Any link to a heading using the portal's {$...} syntax will be reported as broken even though the build assigns that explicit ID; use the same [#$] recognition here.
    scripts/sync-static-site.mjs:250
  • sourcePages() only lists the immediate docs/ directory and filters to file entries, so a future upstream page under a nested directory is silently omitted even though the release check will open a sync PR. That contradicts the comment that newly added pages reach readers automatically; recurse through directory entries (and create matching staging parents) or fail explicitly when nested pages are present.
  • Files reviewed: 45/47 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread .github/workflows/sync-static-site.yml Outdated
Comment thread .agents/upstream-tracking.md Outdated
Eleven findings, two posted and nine suppressed. One was critical.

**The workflow's build check would have failed on its first run.** It ran
`git submodule update --init` without the SSH-to-HTTPS rewrite that
`.gitmodules` requires on a runner, and it omitted `.sources/motoko`, which
the build needs because pages under docs/languages/motoko/ pull code through
`file=<motokoExamples>/...` includes. Without it they render empty *without
failing the build*, so the step would have reported a passing build for a site
with empty Motoko pages. Now identical to build.yml.

**The anchor index recognized fewer explicit ids than the build assigns.**
`plugins/remark-heading-id.mjs` accepts `{#id}` and `{$id}`; the index accepted
only the first, and matched a looser pattern besides, so it could have called a
working link broken. The plugin now exports its pattern and the index uses it,
so they cannot diverge.

**A nested upstream page would have been dropped silently** while the release
check kept reporting the range as synced. Subdirectories now stop the sync with
what to decide.

Five findings were the same class of imprecision in our prose, all worth
fixing: the canister certifying every response is not the same claim as a
visitor receiving a verified one, which depends on the gateway their URL points
at. `certified-data.md`, `certification.md` and the security checklist now say
so, and the checklist no longer asks static-site readers to refuse raw hosts in
`http_request`, which that canister cannot do. Also: the static-site canister
does manage some headers (`Content-Type`, `ETag`, certification, `ic_env`), so
"no headers of its own" became "no default security headers and no CSP"; and
the project tree still labelled `canister.yaml` as asset canister config.

The documented pin procedure and the PR description both still said a release
with no `docs/` changes produces nothing, which the previous commit made false.
Both now describe the pin-only PR.

Not accepted: making `docs/404.mdx` use relative `.md` links. Every link on
that page is root-relative because it has to be: a 404 is served at whatever
URL the reader typed, so a relative link would resolve against a path that does
not exist. The rule it cites is for pages with a stable location.

Refs #396
@marc0olo

Copy link
Copy Markdown
Member Author

Third Copilot review: two posted comments, answered and resolved in their threads, and nine suppressed. All in 72601ca.

Fixed, would have broken or misled

  • scripts/lib/anchors.mjs:37 ({$id} as well as {#id}): right, plugins/remark-heading-id.mjs accepts both, and my index also matched a looser pattern than the plugin, so it could have called a working link broken. The plugin now exports its regex and the index imports it, so the two cannot drift.
  • scripts/sync-static-site.mjs:250 (nested pages silently omitted): fair, and it contradicted the comment promising that a new upstream page reaches readers on its own. A subdirectory now stops the sync and says what to decide, since a nested page needs a placement choice rather than a default.

Fixed, the same class of imprecision in our prose

The canister certifying every response is not the same claim as a visitor receiving a verified one, which depends on the gateway their URL points at. Four findings were versions of that, and all four were right:

  • docs/concepts/certified-data.md:45 and docs/guides/frontends/certification.md:205: both now scope the claim to a verifying hostname.
  • docs/guides/security/data-integrity-and-authenticity.md:626: the sharpest of them. My link sent static-site readers to a bullet telling them to refuse raw hosts in http_request, which that canister cannot do, since the only signal is an unauthenticated Host header. That bullet is now scoped to custom HTTP canisters and the legacy asset canister, with the gateway guidance for static sites.
  • docs/guides/canister-management/troubleshooting.md:103: correct that "no headers of its own" was too broad. It manages Content-Type, ETag, the certification headers and ic_env; what it adds none of is default security headers and CSP. Narrowed.
  • docs/getting-started/project-structure.mdx:121: the project tree still labelled canister.yaml as "Asset canister configuration" while the page teaches static-site. Fixed.

Fixed, my own drift

  • .github/workflows/sync-static-site.yml:61: the PR description still said the workflow opens a PR only when docs/ changes, which the previous commit made false. Corrected, along with the procedure in .agents/upstream-tracking.md that the posted comment flagged.

Not accepted

  • docs/404.mdx:16 (use a relative .md link): this one is wrong, and following it would break the page. Every link on 404.mdx is root-relative because it has to be: a 404 is served at whatever URL the reader typed, so guides/frontends/static-site/overview.md would resolve against a path that does not exist. The linking rule it cites is about pages with a stable location. My change to that page only moved the target off the legacy page and kept the established form.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical sync pagination and additional moderate workflow, documentation, and generated-content issues block approval.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (14)

Previously missed (2) — in code that hasn't changed since the last review.

CONTRIBUTING.md:111

  • This contributor guide says upstreams are tracked three ways and that everything else is watched, but .sources/upstream.json and .agents/upstream-tracking.md define a fourth reference group with no pin and no release issue. That makes the documented verification and maintenance procedure incorrect for the reference repositories. Please describe all four groups here and replace the ambiguous “Bumping either” guidance with the actual sync and pin procedures.
    scripts/validate.js:121
  • This cross-page variant repeats the malformed no heading ... slugs to wording introduced by the anchor validation. Use the same clear has slug wording as the same-page diagnostic so CI identifies the missing heading consistently.

.github/workflows/sync-static-site.yml:54

  • The workflow treats any existing remote branch as proof that an open PR exists. If the push succeeds but gh pr create fails, every later run takes this branch and exits, leaving the release permanently unsynced with no PR. Check for an actual open PR or clean up and reuse orphaned branches before skipping.
          BRANCH="infra/sync-static-site-${TAG}"
          if git ls-remote --exit-code origin "refs/heads/${BRANCH}" > /dev/null 2>&1; then
            echo "Branch $BRANCH already exists, so a PR is likely open. Skipping."
            echo "needed=false" >> $GITHUB_OUTPUT
            exit 0
          fi

CONTRIBUTING.md:108

  • This newly added prose line uses an em dash as punctuation, contrary to the repository's explicit rule in AGENTS.md:182 and the validator's copy conventions. Replace the separator with a colon or semicolon.
- **Synced without a submodule** — a repo whose own pages are published here, pinned under `synced` in [`.sources/upstream.json`](.sources/upstream.json). Nothing is resolved at build time, so there is no submodule to hold the pin. Its own weekly workflow opens the bump PR.

docs/guides/frontends/certification.md:49

  • The blanket Any canister claim treats .icp.net and .raw.icp.net as universal hostnames, but the synced trust model says hostname verification is a property of how a particular gateway is deployed. Custom gateways need not expose either hostname, so this also undercuts the following static-site qualification. Scope the table to the standard ICP gateway or say that a gateway may expose these host types.
Any canister that serves HTTP is reachable through two kinds of hostname: one where the gateway verifies the response certificate before forwarding it, and a `raw` one where it forwards the response without checking:

| Domain | Certification | Notes |
|--------|--------------|-------|
| `<canister-id>.icp.net` | Verified | The gateway checks the proof on every response |

docs/guides/frontends/static-site/access-protection.md:37

  • These commands use the default local environment, but the next paragraph promises an icp.net URL. Following this quick start therefore deploys and configures a local canister while directing the reader to a mainnet hostname. The repository documents icp deploy without an environment as local and icp deploy -e ic as mainnet in docs/guides/canister-management/lifecycle.mdx:82-84; add -e ic to all three commands for this mainnet example, or change the URL and instructions to describe local access. Because this page is generated, apply the change in the certified-assets source before regenerating it.
icp canister call frontend enable_protection '("/login.html")'

# 3. Mint a credential. Here, a chosen "password" valid for ~1 year.
icp canister call frontend issue_token \
  '(record { label = "owner"; ttl_secs = 31536000 : nat32; value = opt "my-passphrase" })'

docs/guides/frontends/static-site/how-it-works.md:10

  • This generated page names the certified-assets canister without linking to its upstream repository, while the new naming rule in AGENTS.md:398 requires the canister name to link there (the overview link is about the task, not the canister). Update the source sentence to link certified-assets to https://github.com/dfinity/certified-assets, then regenerate rather than editing this file.
You don't need any of this to use certified-assets; the [overview](overview.md) is

docs/guides/frontends/static-site/overview.md:63

  • This says the sync uploads and certifies every file in dir, but the synced site-files reference explicitly says _headers and _redirects are configuration and are never uploaded as assets. Clarify that the plugin processes those files as configuration and uploads the remaining content files, then fix the upstream source and regenerate this page.
This installs the canister, then runs the sync plugin to upload and certify every
file in `dir`. Re-running `icp deploy` syncs again: the plugin diffs your directory
against the canister and uploads only what changed.

docs/guides/frontends/static-site/site-files.md:17

  • The absolute wording here contradicts the next section: _headers and _redirects are explicitly not uploaded or served as assets. Please change this to say ordinary files are uploaded and served, while the two special files configure behavior, and apply the correction upstream before regenerating the page.
    docs/guides/frontends/static-site/verifying-contents.md:54
  • This generated example is not valid command output: it contains a Unicode ellipsis while claiming the tool prints 64 hexadecimal characters and nothing else. The repository's documentation rules prohibit invented output, so update certified-assets's source page with a real fixture or an explicit verification TODO, then regenerate this page.
    docs/guides/frontends/static-site/verifying-contents.md:69
  • This second command block also presents fabricated output () rather than a copyable result. Since the exact Candid rendering is not verified, remove the sample or replace it with a fixture-backed output (or an explicit verification TODO) in the upstream page before syncing.
    scripts/sync-static-site.mjs:297
  • This directory is created before fetching, transforming, and validating the pages. On a checkout where the generated target does not yet exist, any later failure leaves an empty docs/guides/frontends/static-site/ directory even though the error path reports that nothing was written and the comment says the tree remains unchanged. Defer creation until the staging swap; the later .sync-staging setup is sufficient.
    scripts/sync-static-site.mjs:223
  • The check claims to validate sidebar.order, but /^\s+order:/m accepts an order nested under any unrelated frontmatter key. A future upstream page could therefore pass this contract without a sidebar.order and sort arbitrarily in the generated group. Parse or scope the check to the sidebar mapping before publishing the page.
    scripts/sync-static-site.mjs:7
  • This description says the sync performs no prose rewrites, but the same script deliberately normalizes punctuation and spelling and rewrites the supported dfx command below. That makes the contract misleading for anyone deciding whether a source change can be safely synced. Qualify this statement to say that only the listed mechanical transformations are applied.
  • Files reviewed: 46/48 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread scripts/sync-static-site.mjs
Fifteen findings. Eight were ours and are fixed here; six are upstream prose in
the synced tree and are the source repo's call; one is wrong.

**The workflow could strand a release forever.** It treated any existing remote
branch as proof of an open PR, so a run that pushed and then failed at
`gh pr create` would have made every later run skip that release, with no PR
and no further attempts. It now skips only on an actually open PR, and deletes
an orphaned branch so the next run retries.

**Sync contract holes:**

- The frontmatter check accepted `order:` nested under any key, so a future page
  could pass the contract without `sidebar.order` and sort arbitrarily. Scoped
  to the `sidebar` mapping.
- The target directory was created before fetching, so on a fresh checkout a
  failure left an empty directory behind while reporting that nothing was
  written. The staging swap creates it now.
- The header comment claimed nothing is rewritten, which stopped being true when
  punctuation normalization and the `dfx` rewrite arrived. It now says only the
  listed mechanical transformations are applied.
- Added a guard for the contents API's 1000-entry cap, which is the real version
  of the reported pagination problem (see below).

**Docs shape:**

- `CONTRIBUTING.md` described three tracking groups when there are four: the
  `reference` group, with no pin and no release issue, was missing, and
  "bumping either" no longer matched three procedures. Also dropped the em
  dashes I had added there.
- `certification.md` claimed "any canister" is reachable on both hostname kinds,
  which overstates it: which hostnames exist, and which verify, is a property of
  the gateway deployment. Scoped to the standard ICP gateway.
- Both anchor diagnostics now read "X has no heading with slug Y" instead of the
  awkward "no heading in X slugs to Y".

Not accepted: the reported critical finding, that the contents API returns 30
entries and needs pagination. Tested against a real directory: 73 entries in a
single response and no Link header, because that endpoint is not paginated. The
documented limit is 1000 entries, which is now guarded.

Refs #396
@marc0olo

Copy link
Copy Markdown
Member Author

Fourth Copilot review: one posted comment, answered and resolved in its thread, and fourteen suppressed. Eight were ours and are fixed in 32b9bd4; six are upstream prose in the synced tree and belong to the source repo; one is wrong.

Fixed, sync logic

  • .github/workflows/sync-static-site.yml:54 (a branch is not proof of a PR): the most valuable finding in the round. A run that pushed and then failed at gh pr create would have made every later run skip that release, permanently, with nothing to review. It now skips only on an actually open PR and deletes an orphaned branch so the next run retries.
  • scripts/sync-static-site.mjs:223 (/^\s+order:/m accepts order under any key): right, a page could have satisfied the contract without sidebar.order and then sorted arbitrarily. Scoped to the sidebar mapping, with a test both ways.
  • scripts/sync-static-site.mjs:297 (the target directory is created before anything is fetched): correct, and it contradicted the error path's own claim. Only the staging swap creates it now.
  • scripts/sync-static-site.mjs:7 (the header says nothing is rewritten): true when written, false once punctuation normalization and the dfx rewrite arrived. It now says only the listed mechanical transformations are applied, each with the rule that forces it.

Fixed, docs shape

  • CONTRIBUTING.md:111 (three groups vs four): correct, and the reference group was the one missing, which is the group a contributor is most likely to touch unknowingly, since it carries no pin. Described, and "bumping either" replaced with the three real procedures.
  • CONTRIBUTING.md:108 (em dash): mine, and the rule is repo-wide even where the validator only scans docs/. Both of the ones I had added there are gone.
  • docs/guides/frontends/certification.md:49 ("any canister" on both hostname kinds): fair, and it undercut the qualification directly below it. Scoped to the standard ICP gateway, with the point that which hostnames exist and which verify is a property of the gateway deployment.
  • scripts/validate.js:121 (awkward diagnostic wording): both now read "X has no heading with slug Y".

Upstream, not ours to change

Six findings are about the prose in the synced pages: the -e ic flags in the access-protection quick start, linking certified-assets to its repo from a generated page, "uploads every file in dir" versus _headers/_redirects being configuration, the same point in site-files.md, and the two elided state-hash outputs. The tree is generated, so hand-editing it is the one thing that must never happen to it, and the maintainer is taking the state-hash wording up in certified-assets directly. Worth noting that the underlying observations are reasonable; they are just addressed in the source repo, not here.

Not accepted

  • The posted finding, that the contents API returns 30 entries by default and needs pagination. It is not a paginated endpoint: a real directory returns 73 entries in one response with no Link header. The genuine limit is 1000 entries, which 32b9bd4 now guards.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

An unresolved critical network-targeting issue and additional moderate and nit findings must be addressed.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (10)

.github/workflows/sync-static-site.yml:38

  • The tag selector also accepts prereleases such as v0.4.0-rc.1, so a prerelease would be treated as the latest release and the workflow could publish documentation from unreleased behavior. This should use the same exact stable-tag pattern as the upstream.json watcher (^v\d+\.\d+\.\d+$) before advancing the pin.
          TAG=$(git -C /tmp/certified-assets tag --sort=-version:refname | grep '^v[0-9]' | head -1)

docs/getting-started/quickstart.md:112

  • The label defines the frontend-canister role as if it were the static-site task. The naming rule added in this PR distinguishes those concepts, so describe what the canister does instead of equating the two.
2. **Frontend canister**: A [static site](../guides/frontends/static-site/overview.md) that serves your React app. It automatically provides the backend's canister ID to your frontend code via a cookie, so the two canisters can communicate without manual configuration.

docs/guides/frontends/asset-canister.md:363

  • This sentence refers to the recipe, but the new naming rule reserves the bare static-site form for neither a reader-facing task nor an explicit recipe name. Use @dfinity/static-site here so readers can distinguish the recipe from the canister and the static-site task.
Also drop any `configuration.version` field: with static-site the recipe version is the canister version.

docs/guides/frontends/asset-canister.md:369

  • This sentence describes the hosting task, not the recipe, so the bare hyphenated static-site conflicts with the naming rule added in this PR. Say a static site here, reserving @dfinity/static-site for the recipe name.
If your app stores user-generated content, keep serving the frontend from static-site and store uploads in a separate canister that the frontend calls. Mixing a mutable file store into your deploy target means every user upload changes what your site is.

docs/guides/frontends/static-site/verifying-contents.md:54

  • This synced page presents an ellipsized SHA as command output, but the repository rule requires output to come from an upstream fixture or be explicitly marked for verification. Please remove this sample or add the verification TODO in the upstream page and regenerate the synced tree; do not hand-edit this generated file.
    docs/guides/frontends/static-site/verifying-contents.md:69
  • This synced page presents an ellipsized icp canister call reply as command output, but the repository rule requires output to come from an upstream fixture or be explicitly marked for verification. Please remove this sample or add the verification TODO in the upstream page and regenerate the synced tree; do not hand-edit this generated file.
    docs/guides/frontends/static-site/verifying-contents.md:68
  • This generated example invokes state_hash without an explicit Candid argument. The static-site docs themselves note that icp canister call opens an interactive prompt when the argument is omitted, so this does not perform the documented non-interactive read for a no-argument method. Please fix the upstream page and resync it as state_hash '()' -e ic.
    docs/guides/security/data-integrity-and-authenticity.md:626
  • This recommendation tells readers to inspect http_request on the legacy asset canister, but that is a prebuilt canister and users cannot change its handler. The legacy path is configured with allow_raw_access: false in .ic-assets.json5; reserve the http_request check for custom HTTP canisters so the documented remediation is actionable.
    scripts/sync-static-site.mjs:386
  • If the process is terminated after the old tree is renamed at line 384 but before line 385 completes, docs/guides/frontends/static-site/ is absent and the previous pages are left only under ignored scratch space. A canceled workflow or interrupted local sync then cannot build until the sync is rerun; add a rollback/cleanup path or another atomic replacement strategy for this interruption window.
    scripts/sync-static-site.mjs:25
  • The generator copies certified-assets documentation into this repository but only adds provenance and a do-not-edit marker; it does not carry the upstream license alongside the generated files or add an attribution entry to NOTICE. That conflicts with the repository's explicit copied-content policy in .agents/upstream-tracking.md:41-44, so add the license/notice handling to the sync design before publishing this tree.
  • Files reviewed: 46/48 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread docs/guides/frontends/asset-canister.md Outdated
Eleven findings. Six were ours and are fixed here; three are upstream prose;
one is a policy question for the maintainer; one I extended to a second
instance it missed.

**The command we publish was subtly wrong.** The dfx rewrite produced
`icp canister call <id> state_hash -e ic`, and `icp canister call` with no
argument opens an interactive prompt rather than sending an empty one. dfx
infers `()`; icp does not. The rewrite now emits `state_hash '()' -e ic`.

**A prerelease could have advanced the pin.** The workflow selected tags with
`^v[0-9]`, so `v0.4.0-rc.1` would have counted as the latest release and
published docs for unreleased behavior, which is the exact thing the pin policy
exists to prevent. Now the same stable-only pattern the upstream.json watcher
uses.

**Two migration commands targeted the local replica.** The `authorize` call and
the reinstall in "Migrate to a static site" both run against an already
deployed canister, so without `-e ic` they would have hit local and silently
done nothing useful. The review flagged the first; the second is the same
mistake four lines up.

**A remediation was not actionable on the canister it named.** The security
checklist told readers to check `http_request` on the legacy asset canister,
which is prebuilt and has no handler of theirs to change. That path is
`allow_raw_access: false`; the `http_request` check belongs to custom HTTP
canisters.

Also: the tree swap now restores the previous tree if the second rename fails,
rather than leaving the target absent; and three sentences used the bare
`static-site` where the naming rule wants either the recipe (`@dfinity/static-site`)
or the task ("a static site").

Raised rather than decided: the review asks for upstream license text and a
NOTICE entry for the synced tree. certified-assets is DFINITY Apache-2.0, the
same license and holder as this repo, and neither existing synced tree is in
NOTICE, including `docs/languages/motoko/`, which comes from another org. That
is a repo-wide question rather than something to settle for one tree in this PR.

Refs #396
@marc0olo

Copy link
Copy Markdown
Member Author

Fifth Copilot review: one posted comment, answered in its thread, and ten suppressed. Six were ours, fixed in 1343a13. Three are upstream prose in the generated tree. One is a policy question worth a maintainer's decision rather than a fix here.

Fixed, and two of these were producing wrong instructions

  • docs/guides/frontends/static-site/verifying-contents.md:68 (state_hash with no Candid argument): the best finding of the round, because the defect was in our rewrite rather than upstream. icp canister call with no argument opens an interactive prompt instead of sending an empty one, which dfx infers. The rewrite now emits state_hash '()' -e ic.
  • .github/workflows/sync-static-site.yml:38 (prerelease tags): correct, ^v[0-9] would have treated v0.4.0-rc.1 as the latest release and published documentation for unreleased behavior, which is exactly what the pin policy exists to prevent. Now the same stable-only pattern the upstream.json watcher uses.
  • docs/guides/security/data-integrity-and-authenticity.md:626: right that the remediation was not actionable where it pointed. The legacy asset canister is prebuilt, so there is no http_request of the reader's to change; that path is allow_raw_access: false, and the handler check belongs to custom HTTP canisters.
  • scripts/sync-static-site.mjs:386 (the window between the two renames): the swap now restores the previous tree if the second rename fails, so the target is never left absent.
  • docs/getting-started/quickstart.md:112, docs/guides/frontends/asset-canister.md:363 and :369: fair against the naming rule this PR adds. Each now uses either the recipe (@dfinity/static-site) or the task ("a static site").

Extended beyond what was flagged

The posted finding about the missing -e ic on authorize applies equally to the reinstall four lines above it, which is the more consequential one since it discards canister state. Both are explicit now.

Upstream, not ours to change

The two elided state-hash outputs, which the maintainer is handling in certified-assets directly.

Raised rather than decided: license and NOTICE

The suggestion is to carry the upstream license alongside the generated files and add a NOTICE entry, citing this repo's copied-content policy. Worth a decision, but not one to make for a single tree inside this PR:

  • dfinity/certified-assets is Apache-2.0, the same license and the same copyright holder as this repository.
  • Neither existing synced tree has a NOTICE entry, including docs/languages/motoko/, which comes from a different org (caffeinelabs, Apache-2.0) and is therefore the stronger case.

So either the policy means third-party in the ownership sense and none of the three needs an entry, or all three do and that is a small separate change. Adding one only for certified-assets would leave the repo in the least coherent of the three states.

@marc0olo
marc0olo merged commit 6a5d040 into main Sep 17, 2026
8 checks passed
@marc0olo
marc0olo deleted the docs/frontends-static-site branch September 17, 2026 06:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

infra: sync certified-assets docs into Frontends and make static-site the primary path

4 participants