From b47a3c05b2339220d2f9b696a6fff6b6b5d5c474 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Tue, 15 Sep 2026 19:34:21 +0100 Subject: [PATCH 1/4] DOC: The PMP resource key is the file name in the tag's URL The loader is served at /api/v4/pmp/{resource}.js and each bundle at /api/v4/pmp/{resource}/{name}, so the key is a path segment, the same shape as every other keyed request, and data-resource-key is gone. Both tag snippets, the attribute table and the load sequence diagram follow. The integration page says plainly what the old shape was and that it is answered 404 now, because a reader arriving with a working page from an earlier release needs to know why it stopped rather than be left to guess. --- src/identifiers/pmp/configuration.md | 21 +++++++++++++-------- src/identifiers/pmp/index.md | 2 +- src/identifiers/pmp/integration.md | 22 ++++++++++++++-------- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/identifiers/pmp/configuration.md b/src/identifiers/pmp/configuration.md index ee3466a0c..088e29218 100644 --- a/src/identifiers/pmp/configuration.md +++ b/src/identifiers/pmp/configuration.md @@ -1,16 +1,22 @@ @page Identifiers_PMP_Configuration Configuration Attributes -Every setting is a `data-` attribute on the one ` ``` -The platform's own URL names the loader and carries nothing else. Every -setting, the resource key included, is a `data-` attribute on the tag, so -each setting is written once and read from one place. The full list is on +The resource key is the file name in the PMP's own URL, which is the shape +every other keyed request takes, and it is read from there and from nowhere +else. Every other setting is a `data-` attribute on the tag, so each setting +is written once and read from one place. The full list is on @ref Identifiers_PMP_Configuration. +Up to 4.4.37 the key was the `data-resource-key` attribute and the URL was +`/api/v4/pmp` with nothing after it. Neither is served now. Two places to +write a key meant a page could carry one the cloud never saw, and the +refusal that followed was invisible to the page, so a tag in the old shape +is answered 404 on its first request instead. + Your resource key must be registered for the domain the page is served from. The request for the platform's bundle is checked against the domains the key names, the same as every other keyed endpoint, so a page on a domain @@ -131,8 +138,7 @@ optional `data-object-name` attribute. Leave the attribute out and `fod` is used, which is what almost every page wants. ```{html} - From 0f1e3767ff749f5e478329fc41edf4a49c33543b Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Tue, 15 Sep 2026 20:15:16 +0100 Subject: [PATCH 2/4] DOC: Say PMP, not "the platform", through the PMP pages 101 uses across eight pages, against the rule that PMP is the name and "the platform" is not, which matters most here because these pages also talk about a consent management platform and the reader has to be able to tell which one a sentence means. What stays. "Consent management platform" and "Preference Management Platform" are untouched, being the full names. So is every "your platform", "whose platform" and "that platform" in the consent management platform page, because those mean the reader's own platform and not this one. So is the console message quoted there, because the client script really writes those words and the documentation has to match. One line in that page said "the platform itself may load later" about the reader's consent management platform, which now names it, because a sentence that could be read either way is exactly what this rule is for. --- src/identifiers/pmp/cmp-comparison.md | 36 ++++++++++---------- src/identifiers/pmp/cmp-wiring.md | 7 ++-- src/identifiers/pmp/configuration.md | 14 ++++---- src/identifiers/pmp/index.md | 34 +++++++++---------- src/identifiers/pmp/integration.md | 48 +++++++++++++-------------- src/identifiers/pmp/isgdpr.md | 12 +++---- src/identifiers/pmp/preferences.md | 28 ++++++++-------- src/identifiers/pmp/sharing.md | 36 ++++++++++---------- 8 files changed, 108 insertions(+), 107 deletions(-) diff --git a/src/identifiers/pmp/cmp-comparison.md b/src/identifiers/pmp/cmp-comparison.md index a4f3220d0..346a441d7 100644 --- a/src/identifiers/pmp/cmp-comparison.md +++ b/src/identifiers/pmp/cmp-comparison.md @@ -6,7 +6,7 @@ tags on your page as a consent management platform. It installs `window.__tcfapi`, the `__tcfapiLocator` frame and the cross frame message handler, and it hands out a TC string in the framework's format, so advertising code that already reads a consent management platform reads the -platform the same way. +PMP the same way. It does not follow the Transparency and Consent Framework Policies of IAB Europe. It is not registered with IAB Europe as a consent management @@ -19,8 +19,8 @@ What the answers to that question mean is set by the Model Terms for Marketing. The Model Terms prescribe, in Appendix 1, how the framework's purposes map onto the two marketing types, standard marketing and personalized marketing, and those two types are two of the three answers -the platform offers. The text in force is version 2, at -, and the platform's purpose sets are built from +the PMP offers. The text in force is version 2, at +, and the PMP's purpose sets are built from that appendix. The rest of the scheme, being who the parties are, what a receiver may do and why the protection is a contract rather than a property of the identifier, is in the explainer at , and this @@ -30,7 +30,7 @@ page does not repeat it. - IAB Europe, the Transparency and Consent Framework: -- IAB Europe, the Framework Policies, which the platform does not follow: +- IAB Europe, the Framework Policies, which the PMP does not follow: - IAB Europe, the framework for consent management platforms, which is where registration is described: @@ -47,22 +47,22 @@ page does not repeat it. - The Model Terms for Marketing explainer: -# The Platform Against a Consent Management Platform +# The PMP Against a Consent Management Platform | | A consent management platform under the Framework Policies | The Preference Management Platform | |---|---|---| -| Who sets the policy | IAB Europe, through the Transparency and Consent Framework Policies. | The Model Terms for Marketing, version 2. The platform follows the framework's technical schema and not its Policies. | -| Registration with IAB Europe | Required. IAB Europe's page for consent management platforms says that "CMPs must register to participate in the TCF", and a registered platform appears on the CMP list with a CMP ID of its own. | None. The platform is not registered and has no CMP ID of its own. It uses an ID chosen at random each day from IAB Europe's list of registered consent management platforms, writes that ID into the TC string and reports the same ID through `__tcfapi`. *The CMP ID* below gives the reason. | +| Who sets the policy | IAB Europe, through the Transparency and Consent Framework Policies. | The Model Terms for Marketing, version 2. The PMP follows the framework's technical schema and not its Policies. | +| Registration with IAB Europe | Required. IAB Europe's page for consent management platforms says that "CMPs must register to participate in the TCF", and a registered platform appears on the CMP list with a CMP ID of its own. | None. The PMP is not registered and has no CMP ID of its own. It uses an ID chosen at random each day from IAB Europe's list of registered consent management platforms, writes that ID into the TC string and reports the same ID through `__tcfapi`. *The CMP ID* below gives the reason. | | The question asked | Transparency and choices about the vendors the publisher has chosen to work with and the purposes each vendor wants to use, as IAB Europe's page for consent management platforms describes it. The Framework Policies say the choice on a purpose is to consent or to object, depending on the legal basis for the processing, and the CMP API specification says the Global Vendor List sets what must be disclosed to the visitor. | One question, in the visitor's own language, with up to three answers, being Personalized, Standard where you turn it on, and the alternative you configure. The purposes behind the two marketing answers are fixed by Appendix 1 of the Model Terms and the visitor does not pick among them. | -| Refusal | IAB Europe says the framework lets a user grant or withhold consent and object to processing. The CMP API specification has the platform capture those choices in a TC string and answer the scripts calling it with that string whenever the user has confirmed their choices. | There is no close cross and no reject button. The alternative button records `non-marketing`, which is an answer and not a refusal. No TC string is built for it, and the surface answers `getTCData` and `addEventListener` with `success` false until the visitor chooses Standard or Personalized. | -| Storage | Left to the platform. The TC string and vendor list formats specification, which the CMP API specification points to, says the storage used for a TC string is up to the consent management platform, cookie or not, and IAB Europe's page for consent management platforms says the same. In apps the CMP API specification names `IABTCF_TCString` and `IABTCF_gdprApplies`, among other keys, in `NSUserDefaults` or `SharedPreferences`. | The answer, one of three words, in this site's `localStorage` under `__51d_pmp_pref`, or in the cloud's own cookie when the visitor agrees to share it across your group. The TC string is built in memory from that answer on every page load and is never written to a cookie or to storage. | +| Refusal | IAB Europe says the framework lets a user grant or withhold consent and object to processing. The CMP API specification has the PMP capture those choices in a TC string and answer the scripts calling it with that string whenever the user has confirmed their choices. | There is no close cross and no reject button. The alternative button records `non-marketing`, which is an answer and not a refusal. No TC string is built for it, and the surface answers `getTCData` and `addEventListener` with `success` false until the visitor chooses Standard or Personalized. | +| Storage | Left to the PMP. The TC string and vendor list formats specification, which the CMP API specification points to, says the storage used for a TC string is up to the consent management platform, cookie or not, and IAB Europe's page for consent management platforms says the same. In apps the CMP API specification names `IABTCF_TCString` and `IABTCF_gdprApplies`, among other keys, in `NSUserDefaults` or `SharedPreferences`. | The answer, one of three words, in this site's `localStorage` under `__51d_pmp_pref`, or in the cloud's own cookie when the visitor agrees to share it across your group. The TC string is built in memory from that answer on every page load and is never written to a cookie or to storage. | | The identifier created | None of its own. On a page running a consent management platform the 51Degrees client script sends the TC string to the cloud, which derives the usage from the purposes granted, and the 51Did records that with the signal source bit set. | Every answer, `non-marketing` included, reaches the cloud as a stated `id.usage`, sent by the 51Degrees client script, and the 51Did records that the usage was stated directly, so the signal source bit is clear. | -| The legal basis of the answer | Consent or legitimate interests, which the Framework Policies name as the two lawful grounds under Article 6 of the General Data Protection Regulation that the framework supports. The CMP API specification has the platform obtain consent or register objections, and gives the framework's objective as helping everyone in the advertising chain comply with that Regulation and the ePrivacy Directive. | The Model Terms usage, which is a contract between the parties handling the identifier and not a consent under the Regulation. The dialog is shown whether or not the Regulation applies to the visit, and `gdprApplies` only changes what the surface reports. | -| What a vendor on the page receives | The TC string and the `TCData` object through `__tcfapi`, carrying that vendor's own consent and legitimate interest signals as the user set them. | The same surface and the same string format. The purpose consents come from the visitor's answer, the legitimate interest bits are set for purposes 2, 7, 8, 9, 10 and 11, and every vendor signal, special feature and publisher field is copied from the string you supply in `data-tcf-vendor` exactly as you encoded it. The platform decides nothing per vendor. | +| The legal basis of the answer | Consent or legitimate interests, which the Framework Policies name as the two lawful grounds under Article 6 of the General Data Protection Regulation that the framework supports. The CMP API specification has the PMP obtain consent or register objections, and gives the framework's objective as helping everyone in the advertising chain comply with that Regulation and the ePrivacy Directive. | The Model Terms usage, which is a contract between the parties handling the identifier and not a consent under the Regulation. The dialog is shown whether or not the Regulation applies to the visit, and `gdprApplies` only changes what the surface reports. | +| What a vendor on the page receives | The TC string and the `TCData` object through `__tcfapi`, carrying that vendor's own consent and legitimate interest signals as the user set them. | The same surface and the same string format. The purpose consents come from the visitor's answer, the legitimate interest bits are set for purposes 2, 7, 8, 9, 10 and 11, and every vendor signal, special feature and publisher field is copied from the string you supply in `data-tcf-vendor` exactly as you encoded it. The PMP decides nothing per vendor. | # The CMP ID -The platform is not registered with IAB Europe, so it has no CMP ID of its +The PMP is not registered with IAB Europe, so it has no CMP ID of its own. It uses an ID chosen at random each day from IAB Europe's list of registered consent management platforms, writes that ID into the TC string, and reports the same ID through `__tcfapi`. @@ -86,11 +86,11 @@ organisation that created it, and a receiver checks the signature against the public key that domain publishes. See @ref Identifiers_51Did and . -# Framework Features and How the Platform Implements Them +# Framework Features and How the PMP Implements Them | Feature of a consent management platform | How the Preference Management Platform implements it | |---|---| -| The `__tcfapi` stub that queues calls until the platform loads | Implemented. Installed when the bundle runs. `ping` is answered at once and every other call is queued until the string is ready, then answered in order. | +| The `__tcfapi` stub that queues calls until the PMP loads | Implemented. Installed when the bundle runs. `ping` is answered at once and every other call is queued until the string is ready, then answered in order. | | The `__tcfapiLocator` frame | Implemented. A hidden frame of that name is added to the body, or on `DOMContentLoaded` where the body is not parsed yet, and never added twice. | | Cross frame calls by `postMessage` | Implemented. A `__tcfapiCall` message is passed to `__tcfapi` and answered with a `__tcfapiReturn` message carrying the same `callId`. | | `ping` | Implemented. Reports `cmpStatus`, `cmpLoaded`, `displayStatus`, `apiVersion` `2.3`, `cmpVersion` 1, `cmpId`, `gvlVersion`, `tcfPolicyVersion` and `gdprApplies`. | @@ -98,14 +98,14 @@ the public key that domain publishes. See @ref Identifiers_51Did and | `removeEventListener` | Implemented, and still answered after the alternative answer, so a listener registered earlier can be taken off. | | `getTCData` | Implemented, although the specification deprecated the command in version 2.2. The purpose consents come from the visitor's answer and every other field is decoded from the string. | | `getVendorList` | Not implemented. Refused with `success` false. | -| `getInAppTCData` | Not implemented. The platform runs on web pages. | +| `getInAppTCData` | Not implemented. The PMP runs on web pages. | | Any other command | Refused with `success` false. | | Events | `tcloaded` when the string is ready, and again when `gdprApplies` changes. `useractioncomplete` on each answer. `cmpuishown` when the dialog is reopened while a string exists, so not on the first showing and not after the alternative answer. | | `gdprApplies` | Reported `true` until `IsGdpr` on the client script's object says otherwise, and a consumer already told `tcloaded` is told again with the corrected value. The dialog is shown either way. See @ref Identifiers_PMP_IsGdpr. | | CMP ID and CMP version | No ID of its own. An ID chosen at random each day from IAB Europe's list of registered consent management platforms, written into the string and reported by the surface, for the reason under *The CMP ID* above. `cmpVersion` is reported as 1 and the string's own version field is whatever `data-tcf-vendor` carries. | | Policy version and vendor list version | The `tcfPolicyVersion` and `vendorListVersion` read from the Global Vendor List when the bundle was built, reported by `ping` and `getTCData`. The string's own version fields are whatever `data-tcf-vendor` carries. | | The Global Vendor List | Not fetched at run time and not shown to the visitor. The build reads the list for the two version numbers and nothing else. | -| Vendors | Not chosen by the platform. Vendor consents, vendor legitimate interests and any disclosed vendors segment are copied from `data-tcf-vendor` unchanged. You generate that string with the framework's own tools to match the vendors you have contracted. | +| Vendors | Not chosen by the PMP. Vendor consents, vendor legitimate interests and any disclosed vendors segment are copied from `data-tcf-vendor` unchanged. You generate that string with the framework's own tools to match the vendors you have contracted. | | Purposes | Set from the answer, by Appendix 1 of the Model Terms. Standard consents to purposes 1, 2, 7, 8 and 11. Personalized consents to purposes 1, 2, 3, 4, 5, 6, 7, 8 and 11. Purposes 9, 10 and 12 are never consented. | | Legitimate interest | The bits for purposes 2, 7, 8, 9, 10 and 11 are set on both marketing answers, meaning no objection is recorded. There is no separate objection control. | | Special features and special purposes | Special feature opt-ins are copied from `data-tcf-vendor`. Special purposes carry no consent or objection signal in a TC string. | @@ -116,11 +116,11 @@ the public key that domain publishes. See @ref Identifiers_51Did and | Storing the TC string | Not stored. The string is rebuilt from the answer on every page load. | | A second layer with per purpose and per vendor controls, and stacks | Not implemented. The dialog offers the three answers and nothing per purpose or per vendor. | | Withdrawing or changing an answer | The bubble reopens the dialog at any time. A new answer is stored, a new string is built, and `useractioncomplete` follows. | -| One platform per page | The platform stops with a console warning when a `__tcfapi` that is not its own is already installed, and never replaces it. See @ref Identifiers_PMP_CmpWiring. | +| One PMP per page | The PMP stops with a console warning when a `__tcfapi` that is not its own is already installed, and never replaces it. See @ref Identifiers_PMP_CmpWiring. | # Find Out More -- Running a consent management platform instead of the platform: +- Running a consent management platform instead of the PMP: @ref Identifiers_PMP_CmpWiring - The three answers, where they are kept and how to read them: @ref Identifiers_PMP_Preferences diff --git a/src/identifiers/pmp/cmp-wiring.md b/src/identifiers/pmp/cmp-wiring.md index 05b6cf234..bfe2a93ed 100644 --- a/src/identifiers/pmp/cmp-wiring.md +++ b/src/identifiers/pmp/cmp-wiring.md @@ -32,7 +32,8 @@ The 51Degrees client script reads your consent management platform through Every consent management platform ships one, and the Framework's own specification already requires it to be first on the page. The stub queues anything asked of it. -- **The platform itself may load later.** That is the normal case and it +- **The consent management platform itself may load later.** That is the + normal case and it works, because the stub holds the registration until the real implementation takes over and then calls back. - **A missing stub has no recovery on that page view.** The client script @@ -138,9 +139,9 @@ is theirs and differs between vendors. @ref Identifiers_51Did_Tcf - The identifier, the usage values and how a consent string maps to one: @ref Identifiers_51Did -- The platform for a site that does not run a consent management platform: +- The PMP for a site that does not run a consent management platform: @ref Identifiers_PMP -- How the platform compares with a consent management platform, feature by +- How the PMP compares with a consent management platform, feature by feature: @ref Identifiers_PMP_CmpComparison - Header bidding with the identifier: @ref Integrations_Prebid - How the client script gathers page values: diff --git a/src/identifiers/pmp/configuration.md b/src/identifiers/pmp/configuration.md index 088e29218..4e653c51a 100644 --- a/src/identifiers/pmp/configuration.md +++ b/src/identifiers/pmp/configuration.md @@ -17,32 +17,32 @@ page on a domain the key does not cover is refused and no dialog appears. | Attribute | Required | Default | What it does | |---|---|---|---| -| `data-tcf-vendor` | Yes | none | Your own Transparency and Consent Framework vendor string. The platform sets the purpose bits and the time fields from the visitor's answer and copies everything else through unchanged. A multi part string such as `core.disclosedvendors` is accepted and the trailing parts are preserved. | +| `data-tcf-vendor` | Yes | none | Your own Transparency and Consent Framework vendor string. The PMP sets the purpose bits and the time fields from the visitor's answer and copies everything else through unchanged. A multi part string such as `core.disclosedvendors` is accepted and the trailing parts are preserved. | | `data-brand-name` | Yes | none | Your brand, shown in the dialog. | | `data-brand-terms-url` | Yes | none | Your privacy or terms page, linked from the dialog. | | `data-alt-name` | Yes | none | The label on the alternative button, for example 'Subscribe' or 'Remove ads'. | | `data-alt-url` | Yes | none | What the alternative button does. An `http` or `https` URL navigates the page. A `javascript:` URL runs inline and the page stays where it is. | | `data-network-name` | When sharing | none | The name of the group your sites belong to. Required for the second card, because a visitor has to be told which sites an answer would apply to. Leaving it out turns sharing off with a warning. See @ref Identifiers_PMP_Sharing. | -| `data-object-name` | No | `fod` | The name of the 51Degrees client script's page object, so the platform can find it. Leave it out unless your client script tag sets `fod-js-object-name` to something else. Every console message names whichever name is in force. | +| `data-object-name` | No | `fod` | The name of the 51Degrees client script's page object, so the PMP can find it. Leave it out unless your client script tag sets `fod-js-object-name` to something else. Every console message names whichever name is in force. | | `data-action-url` | No | none | A hook of your own, fired on every answer, with `{preference}` replaced by `standard`, `personalized` or `non-marketing`. An `http` or `https` URL is added as a script tag, a `javascript:` URL runs inline. Leaving it out means nothing is fired, and nothing else changes. | | `data-license-key` | No | none | Further licence keys, several separated by `+`, where your products need one on top of the resource key. Anyone reading the page can see it, exactly as they could when it sat on a URL. | | `data-brand-logo` | No | none | Your logo, shown in the dialog's header. | | `data-brand-icon` | No | a gear symbol | The round icon on the bubble the dialog collapses to. | | `data-network-logo` | No | none | The group's logo, shown beside your own. | | `data-show-standard` | No | `false` | Set to `true` to offer Standard alongside Personalized and the alternative. | -| `data-use-third-party-cookies` | No | `true` | Whether a visitor who chose Standard or Personalized may be offered the second card. Only the exact string `false` turns it off, so a typo leaves it on rather than quietly removing it. Where it is on and `data-network-name` is present, the second card is shown only where the client script confirms that third party cookies work, and where the client script is still testing the cookie when the first card is answered, the platform waits up to three seconds for that confirmation. Turning it off, or leaving out `data-network-name`, means there is no second card and nothing ever waits between the cards. The platform learns whether third party cookies work from the client script's `device.thirdpartycookiesenabled`, which is a string rather than a boolean, so read @ref Identifiers_PMP_Sharing before testing that value in code of your own. | +| `data-use-third-party-cookies` | No | `true` | Whether a visitor who chose Standard or Personalized may be offered the second card. Only the exact string `false` turns it off, so a typo leaves it on rather than quietly removing it. Where it is on and `data-network-name` is present, the second card is shown only where the client script confirms that third party cookies work, and where the client script is still testing the cookie when the first card is answered, the PMP waits up to three seconds for that confirmation. Turning it off, or leaving out `data-network-name`, means there is no second card and nothing ever waits between the cards. The PMP learns whether third party cookies work from the client script's `device.thirdpartycookiesenabled`, which is a string rather than a boolean, so read @ref Identifiers_PMP_Sharing before testing that value in code of your own. | A URL attribute that is neither a path nor an `http` or `https` address is refused and logged, so a `data:` or `vbscript:` URL never reaches the page. `data-alt-url` and `data-action-url` also accept `javascript:`, because running your own code inline is what they are for. -# How Long the Platform Waits +# How Long the PMP Waits -No attribute sets how long the platform waits. Both times are set once when -the platform is built and are the same on every page. +No attribute sets how long the PMP waits. Both times are set once when +the PMP is built and are the same on every page. -| What the platform waits for | For up to | When the time runs out | +| What the PMP waits for | For up to | When the time runs out | |---|---|---| | The cloud, when the group's shared answer is read at start up and when a visitor's answer is written to it | 1500 milliseconds | A read counts as no shared answer and the dialog is shown. A write counts as refused and the answer is kept with this site. | | The client script to confirm that third party cookies work, once the first card is answered, and only where sharing is configured and the cookie is still being tested | 3000 milliseconds (three seconds) | There is no second card and the answer stays with this site. See @ref Identifiers_PMP_Sharing. | diff --git a/src/identifiers/pmp/index.md b/src/identifiers/pmp/index.md index 71b51d64b..0cb2af360 100644 --- a/src/identifiers/pmp/index.md +++ b/src/identifiers/pmp/index.md @@ -7,7 +7,7 @@ available to the rest of the page. The answer is one of three values, and those three values are exactly the `id.usage` values the 51Degrees cloud takes, so nothing has to be translated between the dialog and the service. -The platform does two things a publisher cannot easily do alone. It asks the +The PMP does two things a publisher cannot easily do alone. It asks the question in the visitor's own language, and it can carry the answer across the other websites in a group you name, so a visitor who has already answered on one of your sites is not asked again on the next one. @@ -30,7 +30,7 @@ where nobody was asked produces no identifier at all. is still testing the cookie when the visitor answers, a waiting ring shows for up to three seconds, and if third party cookies are not confirmed in that time there is no second card and the answer stays with this site. - The platform never waits for the 51Did. Where sharing is not configured + The PMP never waits for the 51Did. Where sharing is not configured there is no second card and no wait. See @ref Identifiers_PMP_Sharing. 3. **The bubble.** After answering, the dialog collapses to a small bubble in the corner. Clicking the bubble reopens the dialog so the visitor can @@ -65,14 +65,14 @@ version 2, at . See - **The loader tag**, which is the one ` ``` -Every console message the platform writes names whichever object name is in +Every console message the PMP writes names whichever object name is in force, so a page using a different name reads messages about that name and not about `fod`. @@ -163,10 +163,10 @@ Use it for your own purposes, for example to tell your analytics that an answer was given. It is no longer how the client script is loaded. The client script hears the -answer through the platform's event and refreshes itself, so pointing the +answer through the PMP's event and refreshes itself, so pointing the action URL at the script's own URL would load a second copy of the script, which replaces the first and warns in the console. Where the action URL -names the cloud script and the object already exists, the platform skips it +names the cloud script and the object already exists, the PMP skips it for that reason. Leaving `data-action-url` out means nothing is fired and nothing is written @@ -204,7 +204,7 @@ is included only for a key that carries them too, so a page whose key has no - What the three answers mean and how to read the one in force: @ref Identifiers_PMP_Preferences -- Every attribute the platform reads: @ref Identifiers_PMP_Configuration +- Every attribute the PMP reads: @ref Identifiers_PMP_Configuration - Sharing an answer across your sites: @ref Identifiers_PMP_Sharing - The identifier the answer leads to: @ref Identifiers_51Did - How the client script gathers page values: @ref PipelineApi_Features_ClientSideEvidence diff --git a/src/identifiers/pmp/isgdpr.md b/src/identifiers/pmp/isgdpr.md index 4dfb66368..a48b23fbb 100644 --- a/src/identifiers/pmp/isgdpr.md +++ b/src/identifiers/pmp/isgdpr.md @@ -51,20 +51,20 @@ section is absent when the key does not request the property, so test that it is there before reading it. Do not carry that habit to the third party cookie result. The other value -the platform reads from the client script, +the PMP reads from the client script, `device.thirdpartycookiesenabled`, is a **string** carrying `'True'` or `'False'`, so a plain truth test on that one is wrong. See @ref Identifiers_PMP_Sharing. -# What the Platform Does With It +# What the PMP Does With It -At start up the platform reads `derived.isgdpr` from the client script's +At start up the PMP reads `derived.isgdpr` from the client script's object and sets `gdprApplies` on its own Framework surface from it. Where -your page carries no client script tag, the platform adds one, and where a +your page carries no client script tag, the PMP adds one, and where a tag is there it waits for that tag to run, which is on @ref Identifiers_PMP_Integration. -Where the value cannot be had, the platform writes a warning to the console +Where the value cannot be had, the PMP writes a warning to the console naming the reason, and leaves `gdprApplies` reporting `true`. Nothing else changes. The reason is one of three. @@ -76,7 +76,7 @@ changes. The reason is one of three. # The Dialog Is Shown Either Way **A `false` value does not stop the dialog and does not stop a 51Did being -created.** The question the platform asks is not a request for consent under +created.** The question the PMP asks is not a request for consent under the General Data Protection Regulation. It is the Model Terms for Marketing usage, which is a contractual question, and the answer is what a recipient of a 51Did is allowed to act on wherever the visitor is. `gdprApplies` is diff --git a/src/identifiers/pmp/preferences.md b/src/identifiers/pmp/preferences.md index 27dc67eb3..f079d210d 100644 --- a/src/identifiers/pmp/preferences.md +++ b/src/identifiers/pmp/preferences.md @@ -27,7 +27,7 @@ lets another site in your group offer its own alternative rather than asking a question the visitor has already answered. There is no fourth answer. The dialog has no close cross, so a visitor -cannot leave the first card unanswered, and the platform never invents an +cannot leave the first card unanswered, and the PMP never invents an answer on a visitor's behalf. # Where the Answer Is Kept @@ -43,7 +43,7 @@ and no stale copy to go wrong. Not confirmed covers a browser that blocks third party cookies, a result that is not known, and a confirmation that did not arrive within the three seconds -the platform waits, all set out on @ref Identifiers_PMP_Sharing. A write to the cloud that +the PMP waits, all set out on @ref Identifiers_PMP_Sharing. A write to the cloud that is refused, or not confirmed within 1500 milliseconds, also leaves the answer in this site's `localStorage`. @@ -79,7 +79,7 @@ answer lives here, and where the answer moved to the group's shared store the cloud's cookie has to go as well, which @ref Identifiers_PMP_Sharing describes. -Reopening the dialog does not need a reload. Call the platform's own method +Reopening the dialog does not need a reload. Call the PMP's own method and the visitor gets the dialog back with their current answer shown. ```{js} @@ -89,7 +89,7 @@ window.__51d_pmp.open(); # Reading the Answer From Your Page There are three ways in, and they exist because a page cannot know whether -the platform's bundle has loaded yet. +the PMP's bundle has loaded yet. ## The Window Event @@ -105,7 +105,7 @@ window.addEventListener('51d-pmp-preference', function (e) { }); ``` -Register the listener before the platform's tag if you can. The event fires +Register the listener before the PMP's tag if you can. The event fires once per answer coming into force, so a listener registered afterwards can miss the announcement and should read the getter as well. @@ -116,13 +116,13 @@ var answer = window.__51d_pmp && window.__51d_pmp.preference(); // 'standard', 'personalized', 'non-marketing', or null when nobody has answered ``` -The getter answers from memory, straight away, with whatever the platform +The getter answers from memory, straight away, with whatever the PMP found when it started, this site's storage and the group's shared answer included. The object exists only once the bundle has loaded, so test for it. ## The Transparency and Consent Framework Surface -The platform exposes a `window.__tcfapi` function, which is how advertising +The PMP exposes a `window.__tcfapi` function, which is how advertising code on the page normally asks about consent, and it answers the standard `ping`, `getTCData`, `addEventListener` and `removeEventListener` commands. @@ -135,7 +135,7 @@ __tcfapi('addEventListener', 2, function (tcData, success) { ``` The string it hands out is built from the vendor string you supply in -`data-tcf-vendor`. The platform sets the purpose bits from the visitor's +`data-tcf-vendor`. The PMP sets the purpose bits from the visitor's answer and the time fields, and copies everything else through unchanged, so a validator sees your own vendor set exactly as you encoded it. @@ -147,13 +147,13 @@ so a validator sees your own vendor set exactly as you encoded it. After the alternative answer the surface answers `addEventListener` and `getTCData` with `success` false, and `ping` reports that no string is loaded. That is only the framework's way of describing an answer that grants -no purposes, and it is not the platform failing and not the visitor +no purposes, and it is not the PMP failing and not the visitor declining to answer. The 51Degrees client script never reads that as the -answer, because it takes the answer from the platform itself, where +answer, because it takes the answer from the PMP itself, where `non-marketing` is a value like any other. Choosing Standard or Personalized afterwards restores the full surface. -# The Platform Is Not a Consent Management Platform +# The PMP Is Not a Consent Management Platform The Preference Management Platform is a technically complete implementation of the Transparency and Consent Framework, and it deliberately does not @@ -162,7 +162,7 @@ about the Model Terms usages. So a site that runs a consent management platform does not add this one. The two never share a page, they would fight over `window.__tcfapi`, and -the platform stops with a console warning when it finds a foreign +the PMP stops with a console warning when it finds a foreign `__tcfapi` already installed rather than replacing it. Where the Framework's Policies are what you need, run a consent management platform and wire the 51Degrees client script to it instead, which is @@ -171,7 +171,7 @@ Policies are what you need, run a consent management platform and wire the # Changing an Answer A visitor can reopen the dialog from the bubble at any time and answer -differently. When they do, the platform announces the new answer, the +differently. When they do, the PMP announces the new answer, the 51Degrees client script asks the cloud again, and a fresh 51Did is created carrying the new usage. Page code registered through the client script's `onChange` callback is called with the new data. The identifier issued @@ -185,7 +185,7 @@ of them is what the Model Terms say. - Carrying one answer across your sites: @ref Identifiers_PMP_Sharing - Running a consent management platform instead: @ref Identifiers_PMP_CmpWiring -- How the platform compares with a consent management platform, feature by +- How the PMP compares with a consent management platform, feature by feature: @ref Identifiers_PMP_CmpComparison - What to tell your visitors in your privacy notice: @ref Identifiers_PMP_Privacy diff --git a/src/identifiers/pmp/sharing.md b/src/identifiers/pmp/sharing.md index d0370eb57..9efe4c1fa 100644 --- a/src/identifiers/pmp/sharing.md +++ b/src/identifiers/pmp/sharing.md @@ -4,7 +4,7 @@ A visitor who has answered on one of your websites should not have to answer again on the next one. The Preference Management Platform can offer to carry one answer across a group of sites you name, so the question is asked once. -This is a feature of this platform. A site running a third party consent +This is a feature of the PMP. A site running a third party consent management platform neither reads nor writes the shared answer and never sees the second card. Nothing stops another vendor implementing the same exchange, because the endpoint and the rules are described here. @@ -42,7 +42,7 @@ The second card is offered when all three of these hold. 51Degrees client script confirms them by testing the cookie, and only a tested `'True'` counts. A `'False'`, a result that is not known, the likely status the script starts with and no result at all each mean no - second card. For the platform to have a tested result, your resource key + second card. For the PMP to have a tested result, your resource key has to carry `ThirdPartyCookiesEnabled` and `ThirdPartyCookiesEnabledJavaScript` beside it, so a key missing either one never shows the second card, and the console names the missing @@ -55,7 +55,7 @@ being answered, and clicking it goes back. ## When the Second Card Waits **When your configuration allows third party cookies and the browser may -support them, the platform waits up to three seconds (3000 milliseconds) +support them, the PMP waits up to three seconds (3000 milliseconds) for the 51Degrees client script to confirm that third party cookies work. If they are not confirmed in that time, the second card is not shown and the answer stays with this site.** @@ -63,13 +63,13 @@ the answer stays with this site.** The three seconds start when the visitor answers the first card. The waiting ring covers the cards for as long as the wait lasts, and while it does the cards are darkened and nothing on them can be pressed. The time is set once -when the platform is built, it is the same on every page, and no attribute +when the PMP is built, it is the same on every page, and no attribute changes it. # The Two URL Attributes diff --git a/src/identifiers/pmp/index.md b/src/identifiers/pmp/index.md index 0cb2af360..906d7e653 100644 --- a/src/identifiers/pmp/index.md +++ b/src/identifiers/pmp/index.md @@ -28,8 +28,9 @@ where nobody was asked produces no identifier at all. in the visitor's browser, a second card asks whether the answer should apply to the other sites in the group you named. Where the client script is still testing the cookie when the visitor answers, a waiting ring shows - for up to three seconds, and if third party cookies are not confirmed in - that time there is no second card and the answer stays with this site. + for up to three seconds from a client script being on the page, and if + third party cookies are not confirmed in that time there is no second + card and the answer stays with this site. The PMP never waits for the 51Did. Where sharing is not configured there is no second card and no wait. See @ref Identifiers_PMP_Sharing. 3. **The bubble.** After answering, the dialog collapses to a small bubble @@ -71,9 +72,9 @@ version 2, at . See - **The 51Degrees client script**, which is the script that gathers the page's values and asks the cloud for its answers, publishing them on a page object named `fod` by default. It listens for the PMP's answer - by itself, so you write no code to join the two. Where your page carries - no client script tag, the PMP adds one. See - @ref Identifiers_PMP_Integration. + by itself, so you write no code to join the two. Where no client script + object has appeared on your page by the time it has loaded, the PMP adds + a client script. See @ref Identifiers_PMP_Integration. - **The cloud**, which serves the loader, the bundle and the client script, holds a shared answer when the visitor agrees to share one, and creates the 51Did. @@ -108,12 +109,12 @@ sequenceDiagram Note over Browser,Bundle: An answer that comes back shows no dialog, only the bubble end - alt The page carries no client script tag + alt The client script's object is on the page, or appears while the bundle waits for it + Bundle->>Bundle: Log that it is waiting for the object, then read the object once it appears + else No object by the time the page has loaded Bundle->>Page: Add the client script tag and log that it did Page->>Cloud: GET /api/v4/[resource key].js Cloud-->>Script: The client script - else A tag is there, whether or not it has run - Bundle->>Bundle: Wait for that tag and log that it is waiting end Note over Bundle,Script: The answer is announced on the window and held behind window.__51d_pmp.preference() @@ -136,7 +137,7 @@ sequenceDiagram else Already known not to work, or never going to be tested Bundle-->>Browser: No second card, and the answer stays with this site else The script is still testing the cookie - Note over Bundle: Waits up to three seconds for that alone, never for the 51Did + Note over Bundle: Waits up to three seconds from a client script being on the page, for that alone and never for the 51Did Bundle-->>Browser: Waiting ring alt Confirmed to work within three seconds Script-->>Bundle: Third party cookies work @@ -170,10 +171,11 @@ sequenceDiagram answer comes back the visitor sees no dialog, only the bubble. The PMP waits up to 1500 milliseconds for that reply, and a reply that has not arrived by then counts as no answer, so the dialog is shown. -4. Where the page carries no 51Degrees client script tag, the PMP adds - one from the same cloud that served it, using the resource key it already - holds, and says so in the console. Where a tag is there it waits for it, - run or not, and adds nothing. +4. The PMP looks for the client script's page object, `fod` or the name + `data-object-name` gives, and waits for it to appear until the page has + loaded, saying so in the console. Where no object has appeared by then it + adds the client script from the same cloud that served it, using the + resource key it already holds, and says so in the console. 5. Any answer in force is announced on the window and is also available from `window.__51d_pmp.preference()`, which answers straight away. 6. The client script reads whatever is available when it is built, registers @@ -193,10 +195,11 @@ sequenceDiagram tested the cookie, which is the case above, the PMP decides at once, showing the second card where the cookie worked, and closing the dialog with the answer kept by this site where it did not. Where the test is - still running, the PMP shows its - waiting ring for up to three seconds. If third party cookies are - confirmed in that time the second card follows, and if they are not - there is no second card and the answer stays with this site. + still running, the PMP shows its waiting ring for up to three seconds, + counted from the later of the answer and a client script being on the + page. If third party cookies are confirmed in that time the second card + follows, and if they are not there is no second card and the answer + stays with this site. 10. If the visitor agrees to share, the PMP writes the answer to the cloud and removes the copy held on this site, so there is one answer and never two. The PMP waits up to 1500 milliseconds for the cloud to diff --git a/src/identifiers/pmp/integration.md b/src/identifiers/pmp/integration.md index b04c02725..8adde4919 100644 --- a/src/identifiers/pmp/integration.md +++ b/src/identifiers/pmp/integration.md @@ -61,7 +61,7 @@ script whatever the order of the tags, and both sides are built for that. the visitor just now, from this site's storage, or from the answer shared across your group. -# When the Page Has No Client Script Tag +# When the Page Has No Client Script The PMP adds one. This is the normal, expected behaviour and it is a convenience, so that a publisher who wants the dialog and nothing else still @@ -73,52 +73,66 @@ the `IsGdpr` value that sets what its own Transparency and Consent Framework surface reports. Rather than keeping a second way of finding those out, it uses the one the client script already has. -**A script is added only where the page carries no client script tag at -all.** Where your page does carry one, the PMP waits for that tag to -run and adds nothing, however the two tags are ordered and whether or not -either has run yet, and it says so in the console. +**The PMP looks for the client script's page object and for nothing else.** +The object is `fod`, or the name `data-object-name` gives. Where the script +came from decides nothing, so a client script served by another 51Degrees +cloud, by a proxy of your own or from a bundle of your own making all +count, because each of them leaves the object. + +A client script tag is ordinarily asynchronous, as the tag above is +written, so it has usually not run when the PMP starts, and an object that +is not there yet is not an object that is not coming. The PMP waits for one +to appear, looking every 50 milliseconds, adds nothing while it waits, and +says so in the console. ``` -A client script tag is already on this page, so the PMP is waiting for it to run rather than adding another. 'fod' will be read from it once it has. +Waiting for the 51Degrees client script to leave 'fod' on this page before adding one, because a tag the page carries is ordinarily asynchronous and may not have run yet. The wait ends when the page has loaded, and after 5000 milliseconds at the latest. ``` -The question is asked of the page rather than of the object, because a tag -is in the page from the moment the browser has read it, whether it has run -or not, while an object exists only once its script has run. Both tags are -asynchronous, so an object that is not there yet says nothing about whether -you wrote a tag. A tag written below the PMP's tag counts too, because -the PMP looks again once the browser has finished reading the page. +The wait ends at the page's load event, by which time every tag the page's +markup carries has run whatever address it names, and after five seconds +at the latest on a page whose load event is very late or never comes. +Where the page has already loaded when the PMP starts, nothing waits and +the script is added straight away. -Only when the page really carries none does the PMP build the script's +Only when no object has appeared by then does the PMP build the script's URL from the cloud that served the PMP and the resource key it already holds, add the tag as an asynchronous script, and write a line in the console saying that it did. That message never prints your resource key or your licence key. +``` +There is no client script object named 'fod' on this page, so the client script is being added from the cloud that served this one. Put the script tag on the page to decide for yourself where it sits and when it runs. +``` + Where your content security policy names a nonce, the tag the PMP adds carries the same nonce the PMP's own tag has, so the policy is satisfied without being loosened. -Two things follow from that. +Three things follow from that. - **Put the client script tag on the page yourself when you want control of its parameters.** The tag the PMP adds carries the defaults. Your own tag can set the object name, turn cookies on with `fod-js-enable-cookies=true`, add a licence key, or sit wherever in the - page you want it. The PMP waits for your tag and adds nothing. + page you want it. The PMP finds its object and adds nothing, and the + sooner the object is there the sooner the second card can be offered, + which @ref Identifiers_PMP_Sharing explains. - **Load the client script once.** Loading it twice replaces the first - instance and its state, and the script says so in the console. The - PMP never causes this, because a tag of your own is a tag it waits - for. + instance and its state, and the script says so in the console. The PMP + never causes this on a page whose object is there by the time the page + has loaded, because it adds a script only where none has appeared by + then. ``` 51Degrees: fod already exists on this page. Loading the script twice replaces it. Load it once and call fod.refresh() to update. ``` -A tag that runs and leaves no object behind, which usually means the name on -`data-object-name` and the name the script was built with disagree, is -warned about and nothing is added. A second copy would run every round twice -and create two identifiers, which is worse than the missing value. +- **Keep `data-object-name` and `fod-js-object-name` the same.** The PMP + looks for the object under the name it was told, so where your own tag + was built with another name it finds nothing, adds a client script under + the name it was told once the page has loaded, and the page then runs + two client scripts and creates two identifiers. Where the PMP can work out neither a cloud origin nor a resource key, which happens when a build is opened from disk rather than served, it writes @@ -166,8 +180,8 @@ It is no longer how the client script is loaded. The client script hears the answer through the PMP's event and refreshes itself, so pointing the action URL at the script's own URL would load a second copy of the script, which replaces the first and warns in the console. Where the action URL -names the cloud script and the object already exists, the PMP skips it -for that reason. +names the cloud script and the client script's object is on the page or on +its way, the PMP skips it and says so in the console. Leaving `data-action-url` out means nothing is fired and nothing is written to the console. The answer is still stored, still announced, and the dialog diff --git a/src/identifiers/pmp/isgdpr.md b/src/identifiers/pmp/isgdpr.md index a48b23fbb..e486d0e7a 100644 --- a/src/identifiers/pmp/isgdpr.md +++ b/src/identifiers/pmp/isgdpr.md @@ -60,9 +60,9 @@ the PMP reads from the client script, At start up the PMP reads `derived.isgdpr` from the client script's object and sets `gdprApplies` on its own Framework surface from it. Where -your page carries no client script tag, the PMP adds one, and where a -tag is there it waits for that tag to run, which is on -@ref Identifiers_PMP_Integration. +no client script object appears on your page by the time it has loaded, +the PMP adds a client script, and where one appears it reads that one, +which is on @ref Identifiers_PMP_Integration. Where the value cannot be had, the PMP writes a warning to the console naming the reason, and leaves `gdprApplies` reporting `true`. Nothing else diff --git a/src/identifiers/pmp/privacy.md b/src/identifiers/pmp/privacy.md index bc68dc974..1bf459756 100644 --- a/src/identifiers/pmp/privacy.md +++ b/src/identifiers/pmp/privacy.md @@ -3,8 +3,8 @@ diff --git a/src/identifiers/pmp/sharing.md b/src/identifiers/pmp/sharing.md index 9efe4c1fa..a024c40e0 100644 --- a/src/identifiers/pmp/sharing.md +++ b/src/identifiers/pmp/sharing.md @@ -60,7 +60,13 @@ for the 51Degrees client script to confirm that third party cookies work. If they are not confirmed in that time, the second card is not shown and the answer stays with this site.** -The three seconds start when the visitor answers the first card. The waiting +The three seconds start at the later of the visitor's answer and a client +script being on the page, whether your own or one the PMP added, because +nothing measures the cookie until one is there. On a page that already has +a client script when the visitor answers, which is the ordinary case, that +is the moment of the answer. On a page still loading it is when the script +arrives, so the ring can be up for the wait for the script and then these +three seconds, one after the other. The waiting ring covers the cards for as long as the wait lasts, and while it does the cards are darkened and nothing on them can be pressed. The time is set once when the PMP is built, it is the same on every page, and no attribute @@ -93,8 +99,9 @@ round that is still going on to create the 51Did never holds the second card back. In practice the wait nearly always means the PMP added the client -script itself, because the page carries no client script tag, and the -script has not tested the cookie by the time the visitor clicks. A page's +script itself, because no client script object had appeared by the time +the page loaded, and the script has not tested the cookie by the time the +visitor clicks. A page's own client script tag that has not tested it yet is waited for in the same way, because what the PMP waits on is the missing result and not who added the script. Putting the tag on the page yourself lets it start sooner, @@ -144,11 +151,11 @@ registered after the first round has ended is called once, straight away, and never again, so it can miss that result. The value is measured once the client script has run the snippet that tests -it, and before that it reports the likely answer for the browser. Where your -page carries no client script tag the PMP adds one so that it has this -answer, and where a tag is there it waits for that tag to run rather than -adding a second copy, which is described on -@ref Identifiers_PMP_Integration. +it, and before that it reports the likely answer for the browser. Where no +client script object appears on your page by the time it has loaded, the +PMP adds a client script so that it has this answer, and where the object +appears it reads that one rather than adding a second copy, which is +described on @ref Identifiers_PMP_Integration. Browsers that block third party cookies, which includes Safari and Firefox with their default settings, never show the second card, because the test From 3e75a8a809078591fefa037fcbdbd1e68441b092 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Wed, 16 Sep 2026 08:42:35 +0100 Subject: [PATCH 4/4] FIX: Fall back to main where an API repo has no branch of the base name The preview clones every API repo at the pull request's base branch, so a documentation change can be previewed against the API change it describes. Where the repo has no branch of that name the clone fails outright and the whole preview fails with it. That happens every time a documentation branch is raised against another documentation branch, because the base name is then a documentation branch name and it means nothing in an API repo. The preview reports fatal: Remote branch not found in upstream origin and stops at the first repo in the map, so none of the documentation is built and the failure says nothing about the documentation itself. Each repo is now asked whether it has the branch, with git ls-remote, and cloned at main where it does not. A repo that does have it is still cloned at it, which is the behaviour the preview is there for, and an examples repo is asked separately from the repo it sits inside because the two can differ. The chosen ref is printed for each, so a preview that built against main rather than a paired branch says so in its own log. git ls-remote exits zero with no output for a branch that is not there, so the emptiness of the output is the test rather than the exit code, which leaves a genuinely unreachable repo still failing the run. --- ci/generate-documentation.ps1 | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/ci/generate-documentation.ps1 b/ci/generate-documentation.ps1 index 42d212f2f..867e80160 100644 --- a/ci/generate-documentation.ps1 +++ b/ci/generate-documentation.ps1 @@ -23,6 +23,23 @@ Write-Host "::group::Cloning API docs" $env:GIT_LFS_SKIP_SMUDGE = 1 # use PR target branch if we're running in a PR, or the current CI branch, or main $ref = $env:GITHUB_BASE_REF ? $env:GITHUB_BASE_REF : $env:GITHUB_REF_NAME ? $env:GITHUB_REF_NAME : 'main' + +# An API repo is cloned at $ref where it has a branch of that name, so a +# documentation change can be previewed against the API change it describes. +# Where it has no such branch, main is used. Without the fallback the clone +# fails and the whole preview fails, which is what a documentation branch +# raised against another documentation branch always hits, because the name +# of a documentation branch means nothing in an API repo. +function Resolve-Ref($url, $preferred) { + if ($preferred -eq 'main') { + return 'main' + } + # ls-remote exits 0 with no output where the branch is absent, so the + # emptiness of the output is the test rather than the exit code. + $found = git ls-remote --heads $url $preferred + return $found ? $preferred : 'main' +} + $repoMap = & $PSScriptRoot/apis.ps1 # repos have to be cloned here since they expect documentation repo to be two levels above them $apis = New-Item -Force -ItemType Directory "apis" @@ -30,10 +47,18 @@ $apis = New-Item -Force -ItemType Directory "apis" New-Item -ItemType SymbolicLink -Force -Target $PWD -Path $apis/documentation | Out-Null foreach ($_ in $repoMap.GetEnumerator()) { $repo, $examples = $_.Key, $_.Value - git clone -b $ref --depth=1 --recurse-submodules --shallow-submodules "https://github.com/$env:GITHUB_REPOSITORY_OWNER/$repo.git" "$apis/$repo" + # Each repo is asked about on its own, because a branch can exist in one + # and not in the next, and an examples repo is a separate repo again. + $repoUrl = "https://github.com/$env:GITHUB_REPOSITORY_OWNER/$repo.git" + $repoRef = Resolve-Ref $repoUrl $ref + Write-Host "$repo at $repoRef" + git clone -b $repoRef --depth=1 --recurse-submodules --shallow-submodules $repoUrl "$apis/$repo" if ($examples) { # clone examples inside their main repo - git clone -b $ref --depth=1 "https://github.com/$env:GITHUB_REPOSITORY_OWNER/$examples.git" "$apis/$repo/$examples" + $examplesUrl = "https://github.com/$env:GITHUB_REPOSITORY_OWNER/$examples.git" + $examplesRef = Resolve-Ref $examplesUrl $ref + Write-Host "$examples at $examplesRef" + git clone -b $examplesRef --depth=1 $examplesUrl "$apis/$repo/$examples" } } Write-Host "::endgroup::"