shopify-upgrade-guard 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ Shopify Upgrade Guard detects documented Shopify platform/API upgrade risks in y
4
4
 
5
5
  > Unofficial open-source developer tooling. Not affiliated with, endorsed by, or certified by Shopify.
6
6
 
7
- This repository is a `0.1.0` candidate. npm publication and a GitHub Release are intentionally pending until the core product passes its release gates.
7
+ This repository contains the `0.2.0` release candidate. It is an offline, evidence-backed scanner for Shopify platform and API upgrade risks.
8
8
 
9
9
  ## Try it locally
10
10
 
@@ -34,17 +34,17 @@ steps:
34
34
  fail-on: warning
35
35
  ```
36
36
 
37
- Inputs: `target`, `fail-on` (`never`, `error`, `warning`, or `info`), and `path`.
37
+ Inputs: `target`, `fail-on` (`never`, `error`, `warning`, or `info`), `fail-on-new`, and `path`.
38
38
 
39
- Outputs: `outcome`, `finding-count`, `error-count`, `current-versions` (the structured inventory), and `result-json`.
39
+ Outputs: `outcome`, `finding-count`, `new-finding-count`, `error-count`, `current-versions` (the structured inventory), and `result-json`.
40
40
 
41
41
  The Action uses the current GitHub `node24` JavaScript Action runtime. Consumers do not install dependencies.
42
42
 
43
43
  ## CLI
44
44
 
45
45
  ```text
46
- node src/cli.js scan [--target YYYY-MM] [--format human|json|sarif] [--fail-on ...]
47
- node src/cli.js baseline [--path DIR]
46
+ node src/cli.js scan [--target YYYY-MM] [--format human|json|sarif] [--fail-on ...] [--fail-on-new]
47
+ node src/cli.js baseline create|check [--path DIR]
48
48
  node src/cli.js rules
49
49
  node src/cli.js versions
50
50
  node src/cli.js explain UG-CHECKOUT-001
@@ -77,6 +77,10 @@ The initial pack is deliberately small and high-precision. Every finding include
77
77
  | UG-REST-001 | Versioned REST Admin URL or explicit REST client marker | [REST Admin API](https://shopify.dev/docs/api/admin-rest) |
78
78
  | UG-VERSION-001 | Unsupported or target-retiring version in the inventory | [API versioning](https://shopify.dev/docs/api/usage/versioning) |
79
79
  | UG-VERSION-002 | Supported version older than latest stable | [API versioning](https://shopify.dev/docs/api/usage/versioning) |
80
+ | UG-CUSTOMER-001 | Customer Account checkout fields/types removed in 2026-10 | [Shopify changelog](https://shopify.dev/changelog/customer-account-api-last-incomplete-checkout-and-checkout-types-removed) |
81
+ | UG-POS-001 | Removed POS `session.currentSession.staffMemberId` | [Shopify changelog](https://shopify.dev/changelog/removed-session-currentsession-staffmemberid-from-pos-ui-extensions-2026-10) |
82
+ | UG-ADMIN-001 | Legacy Admin GraphQL `priceRule` usage | [2026-10 release notes](https://shopify.dev/release-notes/2026-10) |
83
+ | UG-SCRIPT-001 | Script Tag create/update usage | [Script Tag deprecation](https://shopify.dev/changelog/online-store-script-tags-deprecation) |
80
84
 
81
85
  Supported inventory surfaces currently include Admin REST, Admin GraphQL, Checkout UI extensions, Customer Account UI extensions, POS UI extensions, Functions, Shopify app TOML, and recognized Shopify client configuration.
82
86
 
@@ -86,4 +90,4 @@ No telemetry, Shopify credentials, store access, or network access is required f
86
90
 
87
91
  ## Roadmap and limitations
88
92
 
89
- GraphQL schema deprecation validation, richer Functions/Customer Account/POS rules, and exact base-tree PR resolution require additional maintained evidence and are not claimed as implemented. See [ROADMAP.md](ROADMAP.md).
93
+ GraphQL schema-wide deprecation validation and exact base-tree resolved-finding reconstruction require additional maintained evidence and are not claimed as implemented. See [ROADMAP.md](ROADMAP.md).
package/action.yml CHANGED
@@ -12,6 +12,10 @@ inputs:
12
12
  description: never, error, warning, or info
13
13
  required: false
14
14
  default: never
15
+ fail-on-new:
16
+ description: Apply fail-on only to findings introduced by the pull request
17
+ required: false
18
+ default: 'false'
15
19
  path:
16
20
  description: Repository path to scan
17
21
  required: false
@@ -20,6 +24,8 @@ runs:
20
24
  using: node24
21
25
  main: dist/index.js
22
26
  outputs:
27
+ new-finding-count:
28
+ description: Number of findings classified as NEW
23
29
  outcome:
24
30
  description: Whether the configured failure policy passed
25
31
  finding-count:
@@ -1,5 +1,5 @@
1
1
  {
2
- "verifiedAt": "2026-09-27",
2
+ "verifiedAt": "2026-09-30",
3
3
  "source": "https://shopify.dev/docs/api/usage/versioning",
4
4
  "versions": [
5
5
  { "version": "2025-01", "releaseDate": "2025-01-01", "accessibleUntil": "2026-01-16T15:00:00Z", "status": "unsupported" },
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{createRequire as e}from"module";var n={24:n=>{n.exports=e(import.meta.url)("node:fs")},708:n=>{n.exports=e(import.meta.url)("node:process")},242:(e,n,t)=>{t.a(e,(async(e,n)=>{try{var r=t(24);var i=t(708);var s=t(787);var o=t(529);const input=e=>i.env[`INPUT_${e.toUpperCase().replaceAll("-","_")}`]||"";const setOutput=(e,n)=>{const t=`ghadelimiter_${Date.now()}`;r.appendFileSync(i.env.GITHUB_OUTPUT||"/dev/null",`${e}<<${t}\n${n}\n${t}\n`)};const e=input("target")||undefined;const a=input("fail-on")||"never";const c=input("path")||i.env.GITHUB_WORKSPACE||i.cwd();try{const n=await(0,s.S)(c,{target:e});const t=JSON.stringify(n);const u=n.findings.filter((e=>!e.baselined));setOutput("outcome",(0,o.W9)(n,a)?"failure":"success");setOutput("finding-count",String(u.length));setOutput("error-count",String(u.filter((e=>e.severity==="error")).length));setOutput("current-versions",JSON.stringify(n.inventory));setOutput("result-json",t);if(i.env.GITHUB_STEP_SUMMARY)r.appendFileSync(i.env.GITHUB_STEP_SUMMARY,`## Shopify Upgrade Guard\n\n${(0,o.L2)(n)}\n`);for(const e of u)if(e.file&&e.line)console.error(`::warning file=${e.file},line=${e.line},col=${e.column}::${e.title}`);if((0,o.W9)(n,a)){console.error(`::error::Upgrade findings exceeded --fail-on ${a}`);i.exitCode=1}}catch(e){console.error(`::error::${e.message}`);i.exitCode=2}n()}catch(e){n(e)}}),1)},529:(e,n,t)=>{t.d(n,{L2:()=>formatHuman,W9:()=>shouldFail});const r={error:0,warning:1,info:2};function activeFindings(e){return e.findings.filter((e=>!e.baselined))}function formatHuman(e){const n=activeFindings(e);const t=["Shopify Upgrade Guard","",`Target version: ${e.targetVersion}`,`Files scanned: ${e.filesScanned}`,""];t.push("Current repository");t.push(...sectionLines(e.currentProblems.filter((e=>!e.baselined)),"Current debt"));t.push("","Upgrade blockers");t.push(...sectionLines(e.upgradeBlockers.filter((e=>!e.baselined)),"Target migration"));const r=n.filter((e=>e.changeStatus==="introduced"));const i=n.filter((e=>e.changeStatus==="pre-existing"));if(r.length||i.length)t.push("",`Introduced by this PR: ${r.length}`,`Pre-existing: ${i.length}`);if(!n.length)t.push("No upgrade risks found.","","Result: scan passed");else t.push("",`${n.length} active risk${n.length===1?"":"s"} found`,`Baselined: ${e.baselinedCount}`,"","Result: migration recommended");return t.join("\n")}function sectionLines(e,n){if(!e.length)return[`No ${n.toLowerCase()}.`];const t=[];for(const n of[...e].sort(((e,n)=>r[e.severity]-r[n.severity])))t.push(`${n.severity.toUpperCase()} ${n.ruleId}`,n.title,` ${n.file}:${n.line}`,` ${n.reason??n.currentState}`,` Migration: ${n.migration}`,` Source: ${n.source}`,"");return t}function shouldFail(e,n="never"){if(n==="never")return false;return activeFindings(e).some((e=>n==="info"||n==="warning"&&e.severity!=="info"||n==="error"&&e.severity==="error"))}function toSarif(e){const n=new Map;for(const t of activeFindings(e))if(!n.has(t.ruleId))n.set(t.ruleId,{id:t.ruleId,shortDescription:{text:t.title},helpUri:t.source,properties:{severity:t.severity,confidence:t.confidence}});const t=[...n.values()];return{version:"2.1.0",$schema:"https://json.schemastore.org/sarif-2.1.0.json",runs:[{tool:{driver:{name:"Shopify Upgrade Guard",informationUri:"https://github.com/efegokdemir/shopify-upgrade-guard",rules:t}},results:activeFindings(e).map((e=>({ruleId:e.ruleId,ruleIndex:t.findIndex((n=>n.id===e.ruleId)),level:e.severity==="error"?"error":e.severity==="warning"?"warning":"note",message:{text:`${e.title}. ${e.reason??e.migration}`},locations:[{physicalLocation:{artifactLocation:{uri:e.file},region:{startLine:e.line,startColumn:e.column}}}]})))}]}}},787:(n,t,r)=>{r.d(t,{S:()=>scan});const i=e(import.meta.url)("node:fs/promises");const s=e(import.meta.url)("node:path");const o=JSON.parse('{"sP":"https://shopify.dev/docs/api/usage/versioning","e4":[{"version":"2025-01","releaseDate":"2025-01-01","accessibleUntil":"2026-01-16T15:00:00Z","status":"unsupported"},{"version":"2025-04","releaseDate":"2025-04-01","accessibleUntil":"2026-04-16T15:00:00Z","status":"unsupported"},{"version":"2025-07","releaseDate":"2025-07-01","accessibleUntil":"2026-07-16T15:00:00Z","status":"unsupported"},{"version":"2025-10","releaseDate":"2025-10-01","accessibleUntil":"2026-10-16T15:00:00Z","status":"stable"},{"version":"2026-01","releaseDate":"2026-01-01","accessibleUntil":"2027-01-16T15:00:00Z","status":"stable"},{"version":"2026-04","releaseDate":"2026-04-01","accessibleUntil":"2027-04-16T15:00:00Z","status":"stable"},{"version":"2026-07","releaseDate":"2026-07-01","accessibleUntil":"2027-07-16T15:00:00Z","status":"latest-stable"},{"version":"2026-10","releaseDate":"2026-10-01","accessibleUntil":"2027-10-16T15:00:00Z","status":"release-candidate"}]}');const a=o.sP;const c=Object.freeze(o.e4);const u=c.find((e=>e.status==="latest-stable"));const l=/^(\d{4})-(01|04|07|10)$/;function parseVersion(e){if(typeof e!=="string")return null;const n=e.match(l);return n?`${n[1]}-${n[2]}`:null}function compareVersions(e,n){const t=parseVersion(e);const r=parseVersion(n);if(!t||!r)throw new Error(`Cannot compare invalid Shopify versions: ${e}, ${n}`);return t===r?0:t<r?-1:1}function isBefore(e,n){return compareVersions(e,n)<0}function isAtOrAfter(e,n){return compareVersions(e,n)>=0}function versionRecord(e){const n=parseVersion(e);return n?c.find((e=>e.version===n))??null:null}function statusFor(e){const n=parseVersion(e);if(!n)return"unknown";return versionRecord(n)?.status??"unknown/future"}function isKnownVersion(e){return Boolean(versionRecord(e))}function targetRecord(e){const n=parseVersion(e);if(!n)throw new Error(`Invalid target version: ${e}. Expected a quarterly YYYY-MM version (01, 04, 07, or 10).`);return versionRecord(n)??{version:n,status:"unknown/future",accessibleUntil:null}}function becomesUnsupportedBefore(e,n){const t=versionRecord(e);const r=targetRecord(n);return Boolean(t?.accessibleUntil&&t.accessibleUntil.slice(0,7)<=r.version)}function versionSummary(){return c}const d=new Set([".git","node_modules","dist","build","coverage","vendor",".next",".cache"]);const p=new Set([".js",".jsx",".ts",".tsx",".graphql",".gql",".toml",".json"]);async function discover(e,n={}){const t=[];const r=[];const o=n.exclude??[];async function walk(n){let a;try{a=await(0,i.readdir)(n,{withFileTypes:true})}catch(e){if(e.code==="ENOENT")return;throw e}for(const c of a){if(c.name.startsWith(".")&&c.name!==".upgradeguard.json")continue;const a=(0,s.join)(n,c.name);const u=(0,s.relative)(e,a);if(c.isDirectory()){if(!d.has(c.name)&&!isExcluded(u,o))await walk(a);continue}if(c.isSymbolicLink())continue;const l=c.name.endsWith(".toml")?".toml":(0,s.extname)(c.name);if(!p.has(l)||c.name.endsWith(".min.js")||isExcluded(u,o))continue;const f=await(0,i.lstat)(a);if(f.size>1024*1024)continue;const g=await(0,i.readFile)(a,"utf8");const h={path:a,relativePath:u,text:g};t.push(h);r.push(...inventoryFor(h))}}await walk(e);t.sort(((e,n)=>e.relativePath.localeCompare(n.relativePath)));r.sort(compareInventory);return{files:t,inventory:r}}function inventoryFor(e){const n=[];const t=e.relativePath.endsWith(".toml");const r=/(?:^|\/)shopify\.(?:app|extension)\.toml$/.test(e.relativePath);const i=r?matchVersion(e.text,/(?:^|\n)\s*api_version\s*=\s*["'](\d{4}-(?:0[147]|10))["']/g):[];for(const t of i)n.push({surface:surfaceForToml(e.relativePath,e.text),version:t.version,file:e.relativePath,line:t.line,source:"api_version"});if(!t){for(const t of matchVersion(e.text,/\/admin\/api\/(\d{4}-(?:0[147]|10))\/(?!graphql(?:\.json)?\b)/g))n.push({surface:"admin_rest_api",version:t.version,file:e.relativePath,line:t.line,source:"admin_api_url"});for(const t of matchVersion(e.text,/\/admin\/api\/(\d{4}-(?:0[147]|10))\/graphql(?:\.json)?\b/g))n.push({surface:"admin_graphql_api",version:t.version,file:e.relativePath,line:t.line,source:"admin_graphql_url"});if(/shopify/i.test(e.text))for(const t of matchVersion(e.text,/\b(?:apiVersion|api_version)\s*[:=]\s*["'](\d{4}-(?:0[147]|10))["']/g))n.push({surface:"shopify_client",version:t.version,file:e.relativePath,line:t.line,source:"client_configuration"})}return[...new Map(n.map((e=>[`${e.surface}|${e.version}|${e.file}`,e]))).values()]}function matchVersion(e,n){return[...e.matchAll(n)].map((n=>{const t=e.slice(0,n.index+n[0].indexOf(n[1]));return{version:parseVersion(n[1]),line:t.split("\n").length}}))}function surfaceForToml(e,n){const t=e.toLowerCase();if(t.includes("checkout")||/\bblock_progress\b/.test(n))return"checkout_ui_extension";if(t.includes("customer"))return"customer_account_ui_extension";if(t.includes("pos"))return"pos_ui_extension";if(t.includes("function"))return"shopify_function";return t.endsWith("shopify.app.toml")?"shopify_app":"extension"}function compareInventory(e,n){return`${e.file}:${e.line}:${e.surface}`.localeCompare(`${n.file}:${n.line}:${n.surface}`)}function isExcluded(e,n){return n.some((n=>globToRegExp(n).test(e)))}function validateExcludePattern(e){globToRegExp(e);return e}function globToRegExp(e){if(typeof e!=="string"||e.length>200||e.startsWith("/")||e.includes(".."))throw new Error(`Invalid exclude pattern: ${e}`);const n=e.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*\*/g,"§").replace(/\*/g,"[^/]*").replace(/§/g,".*").replace(/\?/g,"[^/]");return new RegExp(`^${n}$`)}const f="2026-07";const g=[{id:"UG-CHECKOUT-001",surface:"checkout_ui_extension",severity:"warning",title:"Buyer journey intercept is deprecated",deprecatedIn:f,confidence:"high",documentationUrl:"https://shopify.dev/changelog/deprecating-the-usebuyerjourneyintercept-api-on-checkout-ui-extensions",migrationUrl:"https://shopify.dev/changelog/deprecating-the-usebuyerjourneyintercept-api-on-checkout-ui-extensions",description:"useBuyerJourneyIntercept and buyerJourney.intercept are deprecated from Checkout UI extension version 2026-07. Migrate validation to a cart and checkout validation Function.",detect(e){if(!/\.(?:[jt]sx?|graphql|gql)$/.test(e.relativePath))return[];return matches(maskCommentsAndStrings(e.text),/\b(?:useBuyerJourneyIntercept|buyerJourney\.intercept)\b/g,"Replace client-side blocking with a cart and checkout validation Function.")},evaluate(e,n){return versionAware(e,this,n)}},{id:"UG-CHECKOUT-002",surface:"checkout_ui_extension",severity:"warning",title:"Checkout block_progress capability is deprecated",deprecatedIn:f,confidence:"high",documentationUrl:"https://shopify.dev/changelog/deprecating-the-usebuyerjourneyintercept-api-on-checkout-ui-extensions",migrationUrl:"https://shopify.dev/changelog/deprecating-the-usebuyerjourneyintercept-api-on-checkout-ui-extensions",description:"The block_progress capability is deprecated from Checkout UI extension version 2026-07 and supports the deprecated buyer journey intercept API.",detect(e){if(!e.relativePath.endsWith("shopify.extension.toml"))return[];return matches(maskTomlComments(e.text),/^\s*block_progress\s*=\s*(?:true|false)\s*$/gm,"Move validation logic to a cart and checkout validation Function.")},evaluate(e,n){return versionAware(e,this,n)}},{id:"UG-REST-001",surface:"admin_rest_api",severity:"warning",title:"REST Admin API usage is legacy",confidence:"high",documentationUrl:"https://shopify.dev/docs/api/admin-rest",migrationUrl:"https://shopify.dev/docs/api/admin-graphql",description:"The REST Admin API is legacy. New public apps must use the GraphQL Admin API; existing integrations should plan migration where applicable.",detect(e){if(!/\.(?:[jt]sx?|graphql|gql)$/.test(e.relativePath))return[];const n=[/\/admin\/api\/(?:\d{4}-(?:0[147]|10)|latest|unstable)\/(?!graphql(?:\.json)?\b)/g];if(/shopify/i.test(e.text))n.push(/\b(?:restResources|Rest\s*Admin|adminRest)\b/g);const t=n.flatMap((n=>matches(e.text,n,"Prefer the GraphQL Admin API for new work and plan migration for this REST integration.")));return t.length?[t[0]]:[]},evaluate(e){return{...e,classification:"current",reason:this.description}}},{id:"UG-VERSION-001",surface:"versioned_api",severity:"error",title:"Shopify API version is unsupported",confidence:"high",documentationUrl:"https://shopify.dev/docs/api/usage/versioning",description:"Shopify may fall forward when a request targets an inaccessible version. Pin to a supported stable version and test the migration.",detect(){return[]},evaluate(e,n){const t=statusFor(e.version);const r=becomesUnsupportedBefore(e.version,n.targetVersion);if(t!=="unsupported"&&!r)return null;return{...e,classification:t==="unsupported"?"current":"target",reason:t==="unsupported"?this.description:`This version becomes inaccessible by target ${n.targetVersion}. ${this.description}`}}},{id:"UG-VERSION-002",surface:"versioned_api",severity:"info",title:"Shopify API version is not the latest stable",confidence:"high",documentationUrl:"https://shopify.dev/docs/api/usage/versioning",description:`The latest bundled stable version is ${u.version}. Older supported versions remain usable, but quarterly upgrades reduce migration risk.`,detect(){return[]},evaluate(e){if(statusFor(e.version)==="stable"&&e.version!==u.version)return{...e,classification:"current",reason:this.description};return null}}];function ruleById(e){return g.find((n=>n.id===e))}function versionAware(e,n,t){const r=t.inventory.find((e=>e.surface==="checkout_ui_extension"))?.version;const i=r&&isAtOrAfter(r,n.deprecatedIn);const s=isAtOrAfter(t.targetVersion,n.deprecatedIn);if(!i&&!s)return null;return{...e,classification:i?"current":"target",reason:i?n.description:`This API becomes deprecated before target ${t.targetVersion}. ${n.description}`}}function matches(e,n,t){return[...e.matchAll(n)].map((n=>{const r=e.slice(0,n.index);return{file:null,line:r.split("\n").length,column:n.index-r.lastIndexOf("\n"),snippet:n[0],guidance:t}}))}function attachFile(e,n){return e.map((e=>({...e,file:n.relativePath})))}function maskCommentsAndStrings(e){return e.replace(/\/\/[^\n]*|\/\*[\s\S]*?\*\/|(['"`])(?:\\.|(?!\1)[^\\])*\1/g,(e=>e.replace(/[^\n]/g," ")))}function maskTomlComments(e){return e.replace(/#[^\n]*/g,(e=>e.replace(/[^\n]/g," ")))}const h=e(import.meta.url)("node:crypto");const v=".upgradeguard-baseline.json";function fingerprint(e){return(0,h.createHash)("sha256").update(`${e.ruleId}|${e.surface}|${e.file}|${e.snippet.replace(/\s+/g," ").trim()}`).digest("hex").slice(0,16)}async function loadBaseline(e){try{const n=JSON.parse(await(0,i.readFile)((0,s.join)(e,v),"utf8"));if(!Array.isArray(n.findings))throw new Error("findings must be an array");return{fingerprints:new Set(n.findings.map((e=>e.fingerprint))),entries:n.findings}}catch(e){if(e.code==="ENOENT")return{fingerprints:new Set,entries:[]};throw new Error(`Invalid ${v}: ${e.message}`)}}async function writeBaseline(e,n){const t={schemaVersion:1,generatedAt:(new Date).toISOString(),findings:n.map((e=>({fingerprint:fingerprint(e),ruleId:e.ruleId,surface:e.surface,file:e.file}))).sort(((e,n)=>e.fingerprint.localeCompare(n.fingerprint)))};await writeFile(join(e,v),`${JSON.stringify(t,null,2)}\n`,"utf8");return t}function applyBaseline(e,n){return e.map((e=>({...e,fingerprint:fingerprint(e),baselined:n.fingerprints.has(fingerprint(e))})))}const m=e(import.meta.url)("node:child_process");async function changedLines(e,n){if(!n||n.includes("..")||!/^[A-Za-z0-9_./-]+$/.test(n))return null;const t=await runGit(e,["diff","--no-ext-diff","--unified=0",`${n}...HEAD`,"--"]);const r=new Map;let i=null;for(const e of t.split("\n")){if(e.startsWith("+++ b/")){i=e.slice(6);continue}const n=e.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);if(n&&i){const e=Number(n[1]);const t=Number(n[2]??1);r.set(i,[...r.get(i)??[],{start:e,end:e+Math.max(t,1)-1}])}}return r}async function runGit(e,n){return new Promise(((t,r)=>{const i=(0,m.spawn)("git",n,{cwd:e,stdio:["ignore","pipe","pipe"]});let s="";let o="";i.stdout.on("data",(e=>{s+=e}));i.stderr.on("data",(e=>{o+=e}));i.on("error",r);i.on("close",(e=>e===0?t(s):r(new Error(`git ${n[0]} failed: ${o.trim()}`))))}))}async function loadConfig(e){try{const n=JSON.parse(await(0,i.readFile)((0,s.join)(e,".upgradeguard.json"),"utf8"));validateConfig(n);return n}catch(e){if(e.code==="ENOENT")return{};throw new Error(`Invalid .upgradeguard.json: ${e.message}`)}}function validateConfig(e){if(e.targetVersion!==undefined&&!parseVersion(e.targetVersion))throw new Error("targetVersion must be a quarterly YYYY-MM version.");if(e.failOn!==undefined&&!["never","error","warning","info"].includes(e.failOn))throw new Error("failOn must be never, error, warning, or info.");if(e.exclude!==undefined&&(!Array.isArray(e.exclude)||e.exclude.some((e=>validateExcludePattern(e)===undefined))))throw new Error("exclude must be an array of repository-relative glob patterns.")}async function scan(e,n={}){const t=await loadConfig(e);const r=n.target??t.targetVersion??u.version;const i=n.failOn??t.failOn??"never";targetRecord(r);const s=n.exclude??t.exclude??[];s.forEach(validateExcludePattern);const{files:o,inventory:a}=await discover(e,{exclude:s});const c={root:e,targetVersion:r,inventory:a};const l=[];for(const e of o)for(const n of g){const t=n.detect(e);const i=t.length?attachFile(t,e):inventoryMatches(n,a,e);for(const e of i){const t=n.evaluate(e,c);if(t)l.push(toFinding(n,t,r))}}const d=await loadBaseline(e);let p=applyBaseline(dedupe(l),d);const f=n.baseRef??process.env.GITHUB_BASE_REF;if(f){try{const n=await changedLines(e,f);p=p.map((e=>({...e,changeStatus:isChanged(e,n)?"introduced":"pre-existing"})))}catch{}}const h=p.filter((e=>!e.baselined));const v=h.filter((e=>e.classification==="current"));const m=h.filter((e=>e.classification==="target"));return{schemaVersion:1,tool:"shopify-upgrade-guard",targetVersion:r,failOn:i,latestStable:u.version,filesScanned:o.length,inventory:a,findings:p,currentProblems:v,upgradeBlockers:m,baselinedCount:p.filter((e=>e.baselined)).length,resolvedBaselineCount:[...d.fingerprints].filter((e=>!p.some((n=>n.fingerprint===e)))).length}}function inventoryMatches(e,n,t){if(!e.id.startsWith("UG-VERSION"))return[];return n.filter((e=>e.file===t.relativePath)).map((e=>({file:e.file,line:e.line,column:1,snippet:`${e.source}: ${e.version}`,version:e.version,guidance:"Update this Shopify version to a supported stable release and test the migration."})))}function toFinding(e,n,t){return{ruleId:e.id,severity:e.severity,title:e.title,surface:e.surface,file:n.file,line:n.line,column:n.column,snippet:n.snippet,targetVersion:t,currentState:e.description,migration:n.guidance,source:e.documentationUrl,migrationUrl:e.migrationUrl??null,confidence:e.confidence,classification:n.classification,reason:n.reason,deprecatedIn:e.deprecatedIn??null,removedIn:e.removedIn??null}}function dedupe(e){return[...new Map(e.map((e=>[`${e.ruleId}|${e.file}|${e.line}|${e.snippet}`,e]))).values()]}function isChanged(e,n){return Boolean(n?.get(e.file)?.some((n=>e.line>=n.start&&e.line<=n.end)))}}};var t={};function __nccwpck_require__(e){var r=t[e];if(r!==undefined){return r.exports}var i=t[e]={exports:{}};var s=true;try{n[e](i,i.exports,__nccwpck_require__);s=false}finally{if(s)delete t[e]}return i.exports}(()=>{var e=typeof Symbol==="function"?Symbol("webpack queues"):"__webpack_queues__";var n=typeof Symbol==="function"?Symbol("webpack exports"):"__webpack_exports__";var t=typeof Symbol==="function"?Symbol("webpack error"):"__webpack_error__";var resolveQueue=e=>{if(e&&e.d<1){e.d=1;e.forEach((e=>e.r--));e.forEach((e=>e.r--?e.r++:e()))}};var wrapDeps=r=>r.map((r=>{if(r!==null&&typeof r==="object"){if(r[e])return r;if(r.then){var i=[];i.d=0;r.then((e=>{s[n]=e;resolveQueue(i)}),(e=>{s[t]=e;resolveQueue(i)}));var s={};s[e]=e=>e(i);return s}}var o={};o[e]=e=>{};o[n]=r;return o}));__nccwpck_require__.a=(r,i,s)=>{var o;s&&((o=[]).d=-1);var a=new Set;var c=r.exports;var u;var l;var d;var p=new Promise(((e,n)=>{d=n;l=e}));p[n]=c;p[e]=e=>(o&&e(o),a.forEach(e),p["catch"]((e=>{})));r.exports=p;i((r=>{u=wrapDeps(r);var i;var getResult=()=>u.map((e=>{if(e[t])throw e[t];return e[n]}));var s=new Promise((n=>{i=()=>n(getResult);i.r=0;var fnQueue=e=>e!==o&&!a.has(e)&&(a.add(e),e&&!e.d&&(i.r++,e.push(i)));u.map((n=>n[e](fnQueue)))}));return i.r?s:getResult()}),(e=>(e?d(p[t]=e):l(c),resolveQueue(o))));o&&o.d<0&&(o.d=0)}})();(()=>{__nccwpck_require__.d=(e,n)=>{for(var t in n){if(__nccwpck_require__.o(n,t)&&!__nccwpck_require__.o(e,t)){Object.defineProperty(e,t,{enumerable:true,get:n[t]})}}}})();(()=>{__nccwpck_require__.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n)})();if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=new URL(".",import.meta.url).pathname.slice(import.meta.url.match(/^file:\/\/\/\w:/)?1:0,-1)+"/";var r=__nccwpck_require__(242);r=await r;
1
+ import{createRequire as e}from"module";var t={24:t=>{t.exports=e(import.meta.url)("node:fs")},708:t=>{t.exports=e(import.meta.url)("node:process")},242:(e,t,n)=>{n.a(e,(async(e,t)=>{try{var r=n(24);var i=n(708);var s=n(787);var o=n(529);const input=e=>i.env[`INPUT_${e.toUpperCase().replaceAll("-","_")}`]||"";const setOutput=(e,t)=>{const n=`ghadelimiter_${Date.now()}`;r.appendFileSync(i.env.GITHUB_OUTPUT||"/dev/null",`${e}<<${n}\n${t}\n${n}\n`)};const e=input("target")||undefined;const a=input("fail-on")||"never";const c=input("fail-on-new")==="true";const l=input("path")||i.env.GITHUB_WORKSPACE||i.cwd();try{const t=await(0,s.S)(l,{target:e,failOn:a,baseRef:i.env.GITHUB_BASE_REF||undefined});const n=JSON.stringify(t);const u=t.findings.filter((e=>!e.baselined));setOutput("outcome",(0,o.W9)(t,a,c)?"failure":"success");setOutput("finding-count",String(u.length));setOutput("new-finding-count",String(u.filter((e=>e.lifecycle==="NEW")).length));setOutput("error-count",String(u.filter((e=>e.severity==="error")).length));setOutput("current-versions",JSON.stringify(t.inventory));setOutput("result-json",n);if(i.env.GITHUB_STEP_SUMMARY)r.appendFileSync(i.env.GITHUB_STEP_SUMMARY,`## Shopify Upgrade Guard\n\n${(0,o.L2)(t)}\n`);for(const e of u)if(e.file&&e.line)console.error(`::warning file=${e.file},line=${e.line},col=${e.column}::${e.title}`);if((0,o.W9)(t,a,c)){console.error(`::error::Upgrade findings exceeded --fail-on ${a}${c?" for NEW findings":""}`);i.exitCode=1}}catch(e){console.error(`::error::${e.message}`);i.exitCode=2}t()}catch(e){t(e)}}),1)},529:(e,t,n)=>{n.d(t,{L2:()=>formatHuman,W9:()=>shouldFail});const r={error:0,warning:1,info:2};function activeFindings(e){return e.findings.filter((e=>!e.baselined))}function formatHuman(e){const t=activeFindings(e);const n=["Shopify Upgrade Guard","",`Target version: ${e.targetVersion}`,`Files scanned: ${e.filesScanned}`,""];n.push("Current repository");n.push(...sectionLines(e.currentProblems.filter((e=>!e.baselined)),"Current debt"));n.push("","Upgrade blockers");n.push(...sectionLines(e.upgradeBlockers.filter((e=>!e.baselined)),"Target migration"));const r=t.filter((e=>e.changeStatus==="introduced"));const i=t.filter((e=>e.changeStatus==="pre-existing"));if(r.length||i.length)n.push("",`Introduced by this PR: ${r.length}`,`Pre-existing: ${i.length}`);if(!t.length)n.push("No upgrade risks found.","","Result: scan passed");else n.push("",`${t.length} active risk${t.length===1?"":"s"} found`,`Baselined: ${e.baselinedCount}`,"","Result: migration recommended");return n.join("\n")}function sectionLines(e,t){if(!e.length)return[`No ${t.toLowerCase()}.`];const n=[];for(const t of[...e].sort(((e,t)=>r[e.severity]-r[t.severity])))n.push(`${t.severity.toUpperCase()} ${t.ruleId}`,t.title,` ${t.file}:${t.line}`,` ${t.reason??t.currentState}`,` Migration: ${t.migration}`,` Source: ${t.source}`,"");return n}function shouldFail(e,t="never",n=false){if(t==="never")return false;return activeFindings(e).filter((e=>!n||e.lifecycle==="NEW")).some((e=>t==="info"||t==="warning"&&e.severity!=="info"||t==="error"&&e.severity==="error"))}function toSarif(e){const t=new Map;for(const n of activeFindings(e))if(!t.has(n.ruleId))t.set(n.ruleId,{id:n.ruleId,shortDescription:{text:n.title},helpUri:n.source,properties:{severity:n.severity,confidence:n.confidence}});const n=[...t.values()];return{version:"2.1.0",$schema:"https://json.schemastore.org/sarif-2.1.0.json",runs:[{tool:{driver:{name:"Shopify Upgrade Guard",informationUri:"https://github.com/efegokdemir/shopify-upgrade-guard",rules:n}},results:activeFindings(e).map((e=>({ruleId:e.ruleId,ruleIndex:n.findIndex((t=>t.id===e.ruleId)),level:e.severity==="error"?"error":e.severity==="warning"?"warning":"note",message:{text:`${e.title}. ${e.reason??e.migration}`},partialFingerprints:{primaryLocationLineHash:e.fingerprint},locations:[{physicalLocation:{artifactLocation:{uri:e.file.replaceAll("\\","/")},region:{startLine:e.line,startColumn:e.column}}}]})))}]}}},787:(t,n,r)=>{r.d(n,{S:()=>scan});const i=e(import.meta.url)("node:fs/promises");const s=e(import.meta.url)("node:path");const o=JSON.parse('{"sP":"https://shopify.dev/docs/api/usage/versioning","e4":[{"version":"2025-01","releaseDate":"2025-01-01","accessibleUntil":"2026-01-16T15:00:00Z","status":"unsupported"},{"version":"2025-04","releaseDate":"2025-04-01","accessibleUntil":"2026-04-16T15:00:00Z","status":"unsupported"},{"version":"2025-07","releaseDate":"2025-07-01","accessibleUntil":"2026-07-16T15:00:00Z","status":"unsupported"},{"version":"2025-10","releaseDate":"2025-10-01","accessibleUntil":"2026-10-16T15:00:00Z","status":"stable"},{"version":"2026-01","releaseDate":"2026-01-01","accessibleUntil":"2027-01-16T15:00:00Z","status":"stable"},{"version":"2026-04","releaseDate":"2026-04-01","accessibleUntil":"2027-04-16T15:00:00Z","status":"stable"},{"version":"2026-07","releaseDate":"2026-07-01","accessibleUntil":"2027-07-16T15:00:00Z","status":"latest-stable"},{"version":"2026-10","releaseDate":"2026-10-01","accessibleUntil":"2027-10-16T15:00:00Z","status":"release-candidate"}]}');const a=o.sP;const c=Object.freeze(o.e4);const l=c.find((e=>e.status==="latest-stable"));const u=/^(\d{4})-(01|04|07|10)$/;function parseVersion(e){if(typeof e!=="string")return null;const t=e.match(u);return t?`${t[1]}-${t[2]}`:null}function compareVersions(e,t){const n=parseVersion(e);const r=parseVersion(t);if(!n||!r)throw new Error(`Cannot compare invalid Shopify versions: ${e}, ${t}`);return n===r?0:n<r?-1:1}function isBefore(e,t){return compareVersions(e,t)<0}function isAtOrAfter(e,t){return compareVersions(e,t)>=0}function versionRecord(e){const t=parseVersion(e);return t?c.find((e=>e.version===t))??null:null}function statusFor(e){const t=parseVersion(e);if(!t)return"unknown";return versionRecord(t)?.status??"unknown/future"}function isKnownVersion(e){return Boolean(versionRecord(e))}function targetRecord(e){const t=parseVersion(e);if(!t)throw new Error(`Invalid target version: ${e}. Expected a quarterly YYYY-MM version (01, 04, 07, or 10).`);return versionRecord(t)??{version:t,status:"unknown/future",accessibleUntil:null}}function becomesUnsupportedBefore(e,t){const n=versionRecord(e);const r=targetRecord(t);return Boolean(n?.accessibleUntil&&n.accessibleUntil.slice(0,7)<=r.version)}function versionSummary(){return c}const d=new Set([".git","node_modules","dist","build","coverage","vendor",".next",".cache"]);const p=new Set([".js",".jsx",".ts",".tsx",".graphql",".gql",".toml",".json"]);async function discover(e,t={}){const n=[];const r=[];const o=t.exclude??[];async function walk(t){let a;try{a=await(0,i.readdir)(t,{withFileTypes:true})}catch(e){if(e.code==="ENOENT")return;throw e}for(const c of a){if(c.name.startsWith(".")&&c.name!==".upgradeguard.json")continue;const a=(0,s.join)(t,c.name);const l=(0,s.relative)(e,a);if(c.isDirectory()){if(!d.has(c.name)&&!isExcluded(l,o))await walk(a);continue}if(c.isSymbolicLink())continue;const u=c.name.endsWith(".toml")?".toml":(0,s.extname)(c.name);if(!p.has(u)||c.name.endsWith(".min.js")||isExcluded(l,o))continue;const f=await(0,i.lstat)(a);if(f.size>1024*1024)continue;const h=await(0,i.readFile)(a,"utf8");const g={path:a,relativePath:l,text:h};n.push(g);r.push(...inventoryFor(g))}}await walk(e);n.sort(((e,t)=>e.relativePath.localeCompare(t.relativePath)));r.sort(compareInventory);return{files:n,inventory:r}}function inventoryFor(e){const t=[];const n=e.relativePath.endsWith(".toml");const r=/(?:^|\/)shopify\.(?:app|extension)\.toml$/.test(e.relativePath);const i=r?matchVersion(e.text,/(?:^|\n)\s*api_version\s*=\s*["'](\d{4}-(?:0[147]|10))["']/g):[];for(const n of i)t.push({surface:surfaceForToml(e.relativePath,e.text),version:n.version,file:e.relativePath,line:n.line,source:"api_version"});if(!n){for(const n of matchVersion(e.text,/\/admin\/api\/(\d{4}-(?:0[147]|10))\/(?!graphql(?:\.json)?\b)/g))t.push({surface:"admin_rest_api",version:n.version,file:e.relativePath,line:n.line,source:"admin_api_url"});for(const n of matchVersion(e.text,/\/admin\/api\/(\d{4}-(?:0[147]|10))\/graphql(?:\.json)?\b/g))t.push({surface:"admin_graphql_api",version:n.version,file:e.relativePath,line:n.line,source:"admin_graphql_url"});if(/shopify/i.test(e.text))for(const n of matchVersion(e.text,/\b(?:apiVersion|api_version)\s*[:=]\s*["'](\d{4}-(?:0[147]|10))["']/g))t.push({surface:"shopify_client",version:n.version,file:e.relativePath,line:n.line,source:"client_configuration"})}return[...new Map(t.map((e=>[`${e.surface}|${e.version}|${e.file}`,e]))).values()]}function matchVersion(e,t){return[...e.matchAll(t)].map((t=>{const n=e.slice(0,t.index+t[0].indexOf(t[1]));return{version:parseVersion(t[1]),line:n.split("\n").length}}))}function surfaceForToml(e,t){const n=e.toLowerCase();if(n.includes("checkout")||/\bblock_progress\b/.test(t))return"checkout_ui_extension";if(n.includes("customer"))return"customer_account_ui_extension";if(n.includes("pos"))return"pos_ui_extension";if(n.includes("function"))return"shopify_function";return n.endsWith("shopify.app.toml")?"shopify_app":"extension"}function compareInventory(e,t){return`${e.file}:${e.line}:${e.surface}`.localeCompare(`${t.file}:${t.line}:${t.surface}`)}function isExcluded(e,t){return t.some((t=>globToRegExp(t).test(e)))}function validateExcludePattern(e){globToRegExp(e);return e}function globToRegExp(e){if(typeof e!=="string"||e.length>200||e.startsWith("/")||e.includes(".."))throw new Error(`Invalid exclude pattern: ${e}`);const t=e.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*\*/g,"§").replace(/\*/g,"[^/]*").replace(/§/g,".*").replace(/\?/g,"[^/]");return new RegExp(`^${t}$`)}const f="2026-07";const h="2026-10";const g=/\.(?:[jt]sx?|graphql|gql)$/;const m=[{id:"UG-CHECKOUT-001",surface:"checkout_ui_extension",severity:"warning",title:"Buyer journey intercept is deprecated",deprecatedIn:f,confidence:"high",documentationUrl:"https://shopify.dev/changelog/deprecating-the-usebuyerjourneyintercept-api-on-checkout-ui-extensions",migrationUrl:"https://shopify.dev/changelog/deprecating-the-usebuyerjourneyintercept-api-on-checkout-ui-extensions",description:"useBuyerJourneyIntercept and buyerJourney.intercept are deprecated from Checkout UI extension version 2026-07. Migrate validation to a cart and checkout validation Function.",detect(e){if(!g.test(e.relativePath))return[];return matches(maskCommentsAndStrings(e.text),/\b(?:useBuyerJourneyIntercept|buyerJourney\.intercept)\b/g,"Replace client-side blocking with a cart and checkout validation Function.")},evaluate(e,t){return versionAware(e,this,t)}},{id:"UG-CHECKOUT-002",surface:"checkout_ui_extension",severity:"warning",title:"Checkout block_progress capability is deprecated",deprecatedIn:f,confidence:"high",documentationUrl:"https://shopify.dev/changelog/deprecating-the-usebuyerjourneyintercept-api-on-checkout-ui-extensions",migrationUrl:"https://shopify.dev/changelog/deprecating-the-usebuyerjourneyintercept-api-on-checkout-ui-extensions",description:"The block_progress capability is deprecated from Checkout UI extension version 2026-07 and supports the deprecated buyer journey intercept API.",detect(e){if(!e.relativePath.endsWith("shopify.extension.toml"))return[];return matches(maskTomlComments(e.text),/^\s*block_progress\s*=\s*(?:true|false)\s*$/gm,"Move validation logic to a cart and checkout validation Function.")},evaluate(e,t){return versionAware(e,this,t)}},{id:"UG-CUSTOMER-001",surface:"customer_account_api",severity:"error",title:"Customer Account checkout types are removed",removedIn:h,confidence:"high",documentationUrl:"https://shopify.dev/changelog/customer-account-api-last-incomplete-checkout-and-checkout-types-removed",migrationUrl:"https://shopify.dev/changelog/customer-account-api-last-incomplete-checkout-and-checkout-types-removed",description:"Customer.lastIncompleteCheckout and the Checkout type subtree are removed in API version 2026-10 with no replacement. Use Storefront cart flows or Customer.orders as appropriate.",detect(e){if(!g.test(e.relativePath))return[];return matches(maskComments(e.text),/\blastIncompleteCheckout\b|\bCheckout\s*\{/g,"Remove the Customer Account checkout field/type usage and migrate to Storefront cart flows or Customer.orders.")},evaluate(e,t){return removalAware(e,this,t)}},{id:"UG-POS-001",surface:"pos_ui_extension",severity:"error",title:"POS currentSession.staffMemberId is removed",removedIn:h,confidence:"high",documentationUrl:"https://shopify.dev/changelog/removed-session-currentsession-staffmemberid-from-pos-ui-extensions-2026-10",migrationUrl:"https://shopify.dev/changelog/removed-session-currentsession-staffmemberid-from-pos-ui-extensions-2026-10",description:"session.currentSession.staffMemberId is removed from POS UI Extensions in API version 2026-10. Use session.staffMember.value?.id or subscribe to staffMember.",detect(e){if(!/\.(?:[jt]sx?)$/.test(e.relativePath))return[];return matches(maskComments(e.text),/\bsession\.currentSession\.staffMemberId\b/g,"Use session.staffMember.value?.id or subscribe to the staffMember signal.")},evaluate(e,t){return removalAware(e,this,t)}},{id:"UG-ADMIN-001",surface:"admin_graphql_api",severity:"warning",title:"Legacy GraphQL priceRule field is removed",removedIn:h,confidence:"medium",documentationUrl:"https://shopify.dev/release-notes/2026-10",migrationUrl:"https://shopify.dev/release-notes/2026-10",description:"Legacy priceRule fields and types are removed from the Admin GraphQL API in 2026-10. Use discountTitle or discountCode where applicable.",detect(e){if(!g.test(e.relativePath))return[];return matches(maskComments(e.text),/\bpriceRule\b/g,"Replace priceRule usage with the supported discountTitle or discountCode fields.")},evaluate(e,t){return removalAware(e,this,t)}},{id:"UG-SCRIPT-001",surface:"online_store_script_tags",severity:"error",title:"Script Tag creation and updates are deprecated",deprecatedIn:h,confidence:"high",documentationUrl:"https://shopify.dev/changelog/online-store-script-tags-deprecation",migrationUrl:"https://shopify.dev/docs/apps/build/online-store/script-tag-deprecation",description:"ScriptTag create/update operations error from 2026-10 and storefront injection stops in 2027-03. Migrate to a theme app extension/app embed block or web pixel.",detect(e){if(!g.test(e.relativePath))return[];return matches(maskComments(e.text),/\b(?:scriptTagCreate|scriptTagUpdate|script_tags)\b/g,"Migrate Script Tag injection to a theme app extension/app embed block or web pixel.")},evaluate(e,t){return removalAware(e,this,t)}},{id:"UG-REST-001",surface:"admin_rest_api",severity:"warning",title:"REST Admin API usage is legacy",confidence:"high",documentationUrl:"https://shopify.dev/docs/api/admin-rest",migrationUrl:"https://shopify.dev/docs/api/admin-graphql",description:"The REST Admin API is legacy. New public apps must use the GraphQL Admin API; existing integrations should plan migration where applicable.",detect(e){if(!/\.(?:[jt]sx?|graphql|gql)$/.test(e.relativePath))return[];const t=[/\/admin\/api\/(?:\d{4}-(?:0[147]|10)|latest|unstable)\/(?!graphql(?:\.json)?\b)/g];if(/shopify/i.test(e.text))t.push(/\b(?:restResources|Rest\s*Admin|adminRest)\b/g);const n=t.flatMap((t=>matches(e.text,t,"Prefer the GraphQL Admin API for new work and plan migration for this REST integration.")));return n.length?[n[0]]:[]},evaluate(e){return{...e,classification:"current",reason:this.description}}},{id:"UG-VERSION-001",surface:"versioned_api",severity:"error",title:"Shopify API version is unsupported",confidence:"high",documentationUrl:"https://shopify.dev/docs/api/usage/versioning",description:"Shopify may fall forward when a request targets an inaccessible version. Pin to a supported stable version and test the migration.",detect(){return[]},evaluate(e,t){const n=statusFor(e.version);const r=becomesUnsupportedBefore(e.version,t.targetVersion);if(n!=="unsupported"&&!r)return null;return{...e,classification:n==="unsupported"?"current":"target",reason:n==="unsupported"?this.description:`This version becomes inaccessible by target ${t.targetVersion}. ${this.description}`}}},{id:"UG-VERSION-002",surface:"versioned_api",severity:"info",title:"Shopify API version is not the latest stable",confidence:"high",documentationUrl:"https://shopify.dev/docs/api/usage/versioning",description:`The latest bundled stable version is ${l.version}. Older supported versions remain usable, but quarterly upgrades reduce migration risk.`,detect(){return[]},evaluate(e){if(statusFor(e.version)==="stable"&&e.version!==l.version)return{...e,classification:"current",reason:this.description};return null}}];function ruleById(e){return m.find((t=>t.id===e))}function versionAware(e,t,n){const r=n.inventory.find((e=>e.surface==="checkout_ui_extension"))?.version;const i=r&&isAtOrAfter(r,t.deprecatedIn);const s=isAtOrAfter(n.targetVersion,t.deprecatedIn);if(!i&&!s)return null;return{...e,classification:i?"current":"target",reason:i?t.description:`This API becomes deprecated before target ${n.targetVersion}. ${t.description}`}}function removalAware(e,t,n){if(!isAtOrAfter(n.targetVersion,t.removedIn??t.deprecatedIn))return null;return{...e,classification:"target",reason:`This usage is affected by target ${n.targetVersion}. ${t.description}`}}function matches(e,t,n){return[...e.matchAll(t)].map((t=>{const r=e.slice(0,t.index);return{file:null,line:r.split("\n").length,column:t.index-r.lastIndexOf("\n"),snippet:t[0],guidance:n}}))}function attachFile(e,t){return e.map((e=>({...e,file:t.relativePath})))}function maskCommentsAndStrings(e){return e.replace(/\/\/[^\n]*|\/\*[\s\S]*?\*\/|(['"`])(?:\\.|(?!\1)[^\\])*\1/g,(e=>e.replace(/[^\n]/g," ")))}function maskComments(e){return e.replace(/\/\/[^\n]*|\/\*[\s\S]*?\*\//g,(e=>e.replace(/[^\n]/g," ")))}function maskTomlComments(e){return e.replace(/#[^\n]*/g,(e=>e.replace(/[^\n]/g," ")))}const v=e(import.meta.url)("node:crypto");const y=".upgradeguard-baseline.json";function fingerprint(e){return(0,v.createHash)("sha256").update(`${e.ruleId}|${e.surface}|${e.file}|${e.snippet.replace(/\s+/g," ").trim()}`).digest("hex").slice(0,16)}async function loadBaseline(e,t=y){try{const n=JSON.parse(await(0,i.readFile)((0,s.join)(e,t),"utf8"));if(!Array.isArray(n.findings)||n.findings.some((e=>typeof e.fingerprint!=="string")))throw new Error("findings must be an array of fingerprinted findings");return{fingerprints:new Set(n.findings.map((e=>e.fingerprint))),entries:n.findings,file:t}}catch(e){if(e.code==="ENOENT")return{fingerprints:new Set,entries:[],file:t};throw new Error(`Invalid ${t}: ${e.message}`)}}async function writeBaseline(e,t,n=y){const r={schemaVersion:1,generatedAt:(new Date).toISOString(),findings:t.map((e=>({fingerprint:fingerprint(e),ruleId:e.ruleId,surface:e.surface,file:e.file}))).sort(((e,t)=>e.fingerprint.localeCompare(t.fingerprint)))};await writeFile(join(e,n),`${JSON.stringify(r,null,2)}\n`,"utf8");return r}function applyBaseline(e,t){return e.map((e=>({...e,fingerprint:fingerprint(e),baselined:t.fingerprints.has(fingerprint(e))})))}const b=e(import.meta.url)("node:child_process");async function changedLines(e,t){if(!t||t.includes("..")||!/^[A-Za-z0-9_./-]+$/.test(t))return null;const n=await runGit(e,["diff","--no-ext-diff","--unified=0",`${t}...HEAD`,"--"]);const r=new Map;let i=null;for(const e of n.split("\n")){if(e.startsWith("+++ b/")){i=e.slice(6);continue}const t=e.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);if(t&&i){const e=Number(t[1]);const n=Number(t[2]??1);r.set(i,[...r.get(i)??[],{start:e,end:e+Math.max(n,1)-1}])}}return r}async function runGit(e,t){return new Promise(((n,r)=>{const i=(0,b.spawn)("git",t,{cwd:e,stdio:["ignore","pipe","pipe"]});let s="";let o="";i.stdout.on("data",(e=>{s+=e}));i.stderr.on("data",(e=>{o+=e}));i.on("error",r);i.on("close",(e=>e===0?n(s):r(new Error(`git ${t[0]} failed: ${o.trim()}`))))}))}async function loadConfig(e){try{const t=JSON.parse(await(0,i.readFile)((0,s.join)(e,".upgradeguard.json"),"utf8"));validateConfig(t);return t}catch(e){if(e.code==="ENOENT")return{};throw new Error(`Invalid .upgradeguard.json: ${e.message}`)}}function validateConfig(e){if(e.targetVersion!==undefined&&!parseVersion(e.targetVersion))throw new Error("targetVersion must be a quarterly YYYY-MM version.");if(e.failOn!==undefined&&!["never","error","warning","info"].includes(e.failOn))throw new Error("failOn must be never, error, warning, or info.");if(e.exclude!==undefined&&(!Array.isArray(e.exclude)||e.exclude.some((e=>validateExcludePattern(e)===undefined))))throw new Error("exclude must be an array of repository-relative glob patterns.");if(e.baseline!==undefined&&(typeof e.baseline!=="string"||e.baseline.startsWith("/")||e.baseline.includes("..")))throw new Error("baseline must be a repository-relative file path.")}async function scan(e,t={}){const n=await loadConfig(e);const r=t.target??n.targetVersion??l.version;const i=t.failOn??n.failOn??"never";targetRecord(r);const s=t.exclude??n.exclude??[];s.forEach(validateExcludePattern);const{files:o,inventory:a}=await discover(e,{exclude:s});const c={root:e,targetVersion:r,inventory:a};const u=[];for(const e of o)for(const t of m){const n=t.detect(e);const i=n.length?attachFile(n,e):inventoryMatches(t,a,e);for(const e of i){const n=t.evaluate(e,c);if(n)u.push(toFinding(t,n,r))}}const d=t.noBaseline?{fingerprints:new Set,entries:[],file:null}:await loadBaseline(e,t.baseline??n.baseline);let p=applyBaseline(dedupe(u),d);const f=t.baseRef??process.env.GITHUB_BASE_REF;if(f){try{const t=await changedLines(e,f);p=p.map((e=>({...e,changeStatus:isChanged(e,t)?"introduced":"pre-existing",lifecycle:isChanged(e,t)?"NEW":"EXISTING"})))}catch{p=p.map((e=>({...e,lifecycle:e.baselined?"EXISTING":"UNCHANGED"})))}}else p=p.map((e=>({...e,lifecycle:e.baselined?"EXISTING":"UNCHANGED"})));const h=p.filter((e=>!e.baselined));const g=h.filter((e=>e.classification==="current"));const v=h.filter((e=>e.classification==="target"));return{schemaVersion:1,tool:"shopify-upgrade-guard",targetVersion:r,failOn:i,latestStable:l.version,filesScanned:o.length,inventory:a,findings:p,currentProblems:g,upgradeBlockers:v,baselinedCount:p.filter((e=>e.baselined)).length,resolvedBaselineCount:[...d.fingerprints].filter((e=>!p.some((t=>t.fingerprint===e)))).length,baselineFile:d.file}}function inventoryMatches(e,t,n){if(!e.id.startsWith("UG-VERSION"))return[];return t.filter((e=>e.file===n.relativePath)).map((e=>({file:e.file,line:e.line,column:1,snippet:`${e.source}: ${e.version}`,version:e.version,guidance:"Update this Shopify version to a supported stable release and test the migration."})))}function toFinding(e,t,n){return{ruleId:e.id,severity:e.severity,title:e.title,surface:e.surface,file:t.file,line:t.line,column:t.column,snippet:t.snippet,targetVersion:n,currentState:e.description,migration:t.guidance,source:e.documentationUrl,migrationUrl:e.migrationUrl??null,confidence:e.confidence,classification:t.classification,reason:t.reason,deprecatedIn:e.deprecatedIn??null,removedIn:e.removedIn??null}}function dedupe(e){return[...new Map(e.map((e=>[`${e.ruleId}|${e.file}|${e.line}|${e.snippet}`,e]))).values()]}function isChanged(e,t){return Boolean(t?.get(e.file)?.some((t=>e.line>=t.start&&e.line<=t.end)))}}};var n={};function __nccwpck_require__(e){var r=n[e];if(r!==undefined){return r.exports}var i=n[e]={exports:{}};var s=true;try{t[e](i,i.exports,__nccwpck_require__);s=false}finally{if(s)delete n[e]}return i.exports}(()=>{var e=typeof Symbol==="function"?Symbol("webpack queues"):"__webpack_queues__";var t=typeof Symbol==="function"?Symbol("webpack exports"):"__webpack_exports__";var n=typeof Symbol==="function"?Symbol("webpack error"):"__webpack_error__";var resolveQueue=e=>{if(e&&e.d<1){e.d=1;e.forEach((e=>e.r--));e.forEach((e=>e.r--?e.r++:e()))}};var wrapDeps=r=>r.map((r=>{if(r!==null&&typeof r==="object"){if(r[e])return r;if(r.then){var i=[];i.d=0;r.then((e=>{s[t]=e;resolveQueue(i)}),(e=>{s[n]=e;resolveQueue(i)}));var s={};s[e]=e=>e(i);return s}}var o={};o[e]=e=>{};o[t]=r;return o}));__nccwpck_require__.a=(r,i,s)=>{var o;s&&((o=[]).d=-1);var a=new Set;var c=r.exports;var l;var u;var d;var p=new Promise(((e,t)=>{d=t;u=e}));p[t]=c;p[e]=e=>(o&&e(o),a.forEach(e),p["catch"]((e=>{})));r.exports=p;i((r=>{l=wrapDeps(r);var i;var getResult=()=>l.map((e=>{if(e[n])throw e[n];return e[t]}));var s=new Promise((t=>{i=()=>t(getResult);i.r=0;var fnQueue=e=>e!==o&&!a.has(e)&&(a.add(e),e&&!e.d&&(i.r++,e.push(i)));l.map((t=>t[e](fnQueue)))}));return i.r?s:getResult()}),(e=>(e?d(p[n]=e):u(c),resolveQueue(o))));o&&o.d<0&&(o.d=0)}})();(()=>{__nccwpck_require__.d=(e,t)=>{for(var n in t){if(__nccwpck_require__.o(t,n)&&!__nccwpck_require__.o(e,n)){Object.defineProperty(e,n,{enumerable:true,get:t[n]})}}}})();(()=>{__nccwpck_require__.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t)})();if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=new URL(".",import.meta.url).pathname.slice(import.meta.url.match(/^file:\/\/\/\w:/)?1:0,-1)+"/";var r=__nccwpck_require__(242);r=await r;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shopify-upgrade-guard",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Offline static analysis for Shopify platform and API upgrade risks.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,7 +29,7 @@
29
29
  "license": "MIT",
30
30
  "repository": {
31
31
  "type": "git",
32
- "url": "https://github.com/efegokdemir/shopify-upgrade-guard.git"
32
+ "url": "git+https://github.com/efegokdemir/shopify-upgrade-guard.git"
33
33
  },
34
34
  "bugs": {
35
35
  "url": "https://github.com/efegokdemir/shopify-upgrade-guard/issues"
@@ -48,5 +48,68 @@
48
48
  "devDependencies": {
49
49
  "@vercel/ncc": "^0.38.3",
50
50
  "c8": "^10.1.3"
51
- }
51
+ },
52
+ "main": "index.js",
53
+ "directories": {
54
+ "doc": "docs",
55
+ "test": "tests"
56
+ },
57
+ "dependencies": {
58
+ "ansi-regex": "^6.4.0",
59
+ "ansi-styles": "^6.2.3",
60
+ "balanced-match": "^4.0.4",
61
+ "brace-expansion": "^5.0.12",
62
+ "cliui": "^8.0.1",
63
+ "color-convert": "^2.0.1",
64
+ "color-name": "^1.1.4",
65
+ "convert-source-map": "^2.0.0",
66
+ "cross-spawn": "^7.0.6",
67
+ "eastasianwidth": "^0.2.0",
68
+ "emoji-regex": "^9.2.2",
69
+ "escalade": "^3.2.0",
70
+ "find-up": "^5.0.0",
71
+ "foreground-child": "^3.3.1",
72
+ "get-caller-file": "^2.0.5",
73
+ "glob": "^10.5.0",
74
+ "has-flag": "^4.0.0",
75
+ "html-escaper": "^2.0.2",
76
+ "is-fullwidth-code-point": "^3.0.0",
77
+ "isexe": "^2.0.0",
78
+ "istanbul-lib-coverage": "^3.2.2",
79
+ "istanbul-lib-report": "^3.0.1",
80
+ "istanbul-reports": "^3.2.0",
81
+ "jackspeak": "^3.4.3",
82
+ "locate-path": "^6.0.0",
83
+ "lru-cache": "^10.4.3",
84
+ "make-dir": "^4.0.0",
85
+ "minimatch": "^10.2.6",
86
+ "minipass": "^7.1.3",
87
+ "p-limit": "^3.1.0",
88
+ "p-locate": "^5.0.0",
89
+ "package-json-from-dist": "^1.0.1",
90
+ "path-exists": "^4.0.0",
91
+ "path-key": "^3.1.1",
92
+ "path-scurry": "^1.11.1",
93
+ "require-directory": "^2.1.1",
94
+ "semver": "^7.8.5",
95
+ "shebang-command": "^2.0.0",
96
+ "shebang-regex": "^3.0.0",
97
+ "signal-exit": "^4.1.0",
98
+ "string-width": "^5.1.2",
99
+ "string-width-cjs": "^4.2.3",
100
+ "strip-ansi": "^7.2.0",
101
+ "strip-ansi-cjs": "^6.0.1",
102
+ "supports-color": "^7.2.0",
103
+ "test-exclude": "^7.0.2",
104
+ "v8-to-istanbul": "^9.3.0",
105
+ "which": "^2.0.2",
106
+ "wrap-ansi": "^8.1.0",
107
+ "wrap-ansi-cjs": "^7.0.0",
108
+ "y18n": "^5.0.8",
109
+ "yargs": "^17.7.3",
110
+ "yargs-parser": "^21.1.1",
111
+ "yocto-queue": "^0.1.0"
112
+ },
113
+ "author": "",
114
+ "homepage": "https://github.com/efegokdemir/shopify-upgrade-guard#readme"
52
115
  }
package/src/action.js CHANGED
@@ -4,11 +4,11 @@ import { scan } from './scan.js';
4
4
  import { formatHuman, shouldFail } from './output.js';
5
5
  const input = (name) => process.env[`INPUT_${name.toUpperCase().replaceAll('-', '_')}`] || '';
6
6
  const setOutput = (name, value) => { const delimiter = `ghadelimiter_${Date.now()}`; fs.appendFileSync(process.env.GITHUB_OUTPUT || '/dev/null', `${name}<<${delimiter}\n${value}\n${delimiter}\n`); };
7
- const target = input('target') || undefined; const threshold = input('fail-on') || 'never'; const root = input('path') || process.env.GITHUB_WORKSPACE || process.cwd();
7
+ const target = input('target') || undefined; const threshold = input('fail-on') || 'never'; const failOnNew = input('fail-on-new') === 'true'; const root = input('path') || process.env.GITHUB_WORKSPACE || process.cwd();
8
8
  try {
9
- const result = await scan(root, { target }); const json = JSON.stringify(result); const active = result.findings.filter((finding) => !finding.baselined);
10
- setOutput('outcome', shouldFail(result, threshold) ? 'failure' : 'success'); setOutput('finding-count', String(active.length)); setOutput('error-count', String(active.filter((item) => item.severity === 'error').length)); setOutput('current-versions', JSON.stringify(result.inventory)); setOutput('result-json', json);
9
+ const result = await scan(root, { target, failOn: threshold, baseRef: process.env.GITHUB_BASE_REF || undefined }); const json = JSON.stringify(result); const active = result.findings.filter((finding) => !finding.baselined);
10
+ setOutput('outcome', shouldFail(result, threshold, failOnNew) ? 'failure' : 'success'); setOutput('finding-count', String(active.length)); setOutput('new-finding-count', String(active.filter((item) => item.lifecycle === 'NEW').length)); setOutput('error-count', String(active.filter((item) => item.severity === 'error').length)); setOutput('current-versions', JSON.stringify(result.inventory)); setOutput('result-json', json);
11
11
  if (process.env.GITHUB_STEP_SUMMARY) fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `## Shopify Upgrade Guard\n\n${formatHuman(result)}\n`);
12
12
  for (const finding of active) if (finding.file && finding.line) console.error(`::warning file=${finding.file},line=${finding.line},col=${finding.column}::${finding.title}`);
13
- if (shouldFail(result, threshold)) { console.error(`::error::Upgrade findings exceeded --fail-on ${threshold}`); process.exitCode = 1; }
13
+ if (shouldFail(result, threshold, failOnNew)) { console.error(`::error::Upgrade findings exceeded --fail-on ${threshold}${failOnNew ? ' for NEW findings' : ''}`); process.exitCode = 1; }
14
14
  } catch (error) { console.error(`::error::${error.message}`); process.exitCode = 2; }
package/src/baseline.js CHANGED
@@ -4,9 +4,9 @@ import { join } from 'node:path';
4
4
 
5
5
  export const BASELINE_FILE = '.upgradeguard-baseline.json';
6
6
  export function fingerprint(finding) { return createHash('sha256').update(`${finding.ruleId}|${finding.surface}|${finding.file}|${finding.snippet.replace(/\s+/g, ' ').trim()}`).digest('hex').slice(0, 16); }
7
- export async function loadBaseline(root) {
8
- try { const parsed = JSON.parse(await readFile(join(root, BASELINE_FILE), 'utf8')); if (!Array.isArray(parsed.findings)) throw new Error('findings must be an array'); return { fingerprints: new Set(parsed.findings.map((item) => item.fingerprint)), entries: parsed.findings }; }
9
- catch (error) { if (error.code === 'ENOENT') return { fingerprints: new Set(), entries: [] }; throw new Error(`Invalid ${BASELINE_FILE}: ${error.message}`); }
7
+ export async function loadBaseline(root, file = BASELINE_FILE) {
8
+ try { const parsed = JSON.parse(await readFile(join(root, file), 'utf8')); if (!Array.isArray(parsed.findings) || parsed.findings.some((item) => typeof item.fingerprint !== 'string')) throw new Error('findings must be an array of fingerprinted findings'); return { fingerprints: new Set(parsed.findings.map((item) => item.fingerprint)), entries: parsed.findings, file }; }
9
+ catch (error) { if (error.code === 'ENOENT') return { fingerprints: new Set(), entries: [], file }; throw new Error(`Invalid ${file}: ${error.message}`); }
10
10
  }
11
- export async function writeBaseline(root, findings) { const payload = { schemaVersion: 1, generatedAt: new Date().toISOString(), findings: findings.map((finding) => ({ fingerprint: fingerprint(finding), ruleId: finding.ruleId, surface: finding.surface, file: finding.file })).sort((a, b) => a.fingerprint.localeCompare(b.fingerprint)) }; await writeFile(join(root, BASELINE_FILE), `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); return payload; }
11
+ export async function writeBaseline(root, findings, file = BASELINE_FILE) { const payload = { schemaVersion: 1, generatedAt: new Date().toISOString(), findings: findings.map((finding) => ({ fingerprint: fingerprint(finding), ruleId: finding.ruleId, surface: finding.surface, file: finding.file })).sort((a, b) => a.fingerprint.localeCompare(b.fingerprint)) }; await writeFile(join(root, file), `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); return payload; }
12
12
  export function applyBaseline(findings, baseline) { return findings.map((finding) => ({ ...finding, fingerprint: fingerprint(finding), baselined: baseline.fingerprints.has(fingerprint(finding)) })); }
package/src/cli.js CHANGED
@@ -4,18 +4,19 @@ import { formatHuman, shouldFail, toSarif } from './output.js';
4
4
  import { rules } from './rules.js';
5
5
  import { versionSummary } from './versions.js';
6
6
  import { writeBaseline } from './baseline.js';
7
+ import packageJson from '../package.json' with { type: 'json' };
7
8
  import process from 'node:process';
8
9
  const args = process.argv.slice(2); const command = args[0] ?? 'scan';
9
10
  const value = (flag) => { const index = args.indexOf(flag); return index >= 0 ? args[index + 1] : undefined; };
10
- if (args.includes('--help') || command === 'help') { console.log('Usage: shopify-upgrade-guard <scan|baseline|rules|versions|explain> [options]\n\nscan options:\n --target YYYY-MM compare against a target version\n --format human|json|sarif\n --fail-on never|error|warning|info\n --exclude GLOB ignore a repository-relative path pattern\n --path DIR repository to scan'); process.exit(0); }
11
- if (args.includes('--version')) { console.log('0.1.0'); process.exit(0); }
11
+ if (args.includes('--help') || command === 'help') { console.log('Usage: shopify-upgrade-guard <scan|baseline|rules|versions|explain> [options]\n\nscan options:\n --target YYYY-MM compare against a target version\n --format human|json|sarif\n --fail-on never|error|warning|info\n --fail-on-new apply threshold only to NEW findings\n --base-ref REF classify findings against a Git base ref\n --exclude GLOB ignore a repository-relative path pattern\n --path DIR repository to scan\n\nbaseline create|check writes or evaluates .upgradeguard-baseline.json'); process.exit(0); }
12
+ if (args.includes('--version')) { console.log(packageJson.version); process.exit(0); }
12
13
  try {
13
14
  if (command === 'rules') { for (const rule of rules) console.log(`${rule.id}\t${rule.surface}\t${rule.severity}\t${rule.title}\n ${rule.documentationUrl}`); process.exit(0); }
14
15
  if (command === 'versions') { for (const item of versionSummary()) console.log(`${item.version} ${item.status} — accessible until ${item.accessibleUntil}`); process.exit(0); }
15
16
  if (command === 'explain') { const rule = rules.find((item) => item.id === args[1]); if (!rule) throw new Error(`Unknown rule: ${args[1]}`); console.log(`${rule.id}: ${rule.title}\n\n${rule.description}\n\nSource: ${rule.documentationUrl}${rule.migrationUrl ? `\nMigration: ${rule.migrationUrl}` : ''}`); process.exit(0); }
16
- if (command === 'baseline') { const root = value('--path') ?? process.cwd(); const result = await scan(root, { target: value('--target') }); const payload = await writeBaseline(root, result.findings); console.log(`Wrote ${payload.findings.length} findings to .upgradeguard-baseline.json`); process.exit(0); }
17
+ if (command === 'baseline') { const root = value('--path') ?? process.cwd(); const result = await scan(root, { target: value('--target'), noBaseline: true }); if ((args[1] ?? 'create') === 'check') { console.log(formatHuman(result)); process.exitCode = shouldFail(result, value('--fail-on') ?? 'never', args.includes('--fail-on-new')) ? 1 : 0; } else { const payload = await writeBaseline(root, result.findings); console.log(`Wrote ${payload.findings.length} findings to .upgradeguard-baseline.json`); process.exitCode = 0; } process.exit(process.exitCode ?? 0); }
17
18
  if (command !== 'scan') throw new Error(`Unknown command: ${command}`);
18
- const root = value('--path') ?? process.cwd(); const excludes = args.flatMap((arg, index) => arg === '--exclude' ? [args[index + 1]] : []).filter(Boolean); const result = await scan(root, { target: value('--target'), failOn: value('--fail-on'), exclude: excludes.length ? excludes : undefined }); const format = value('--format') ?? 'human';
19
+ const root = value('--path') ?? process.cwd(); const excludes = args.flatMap((arg, index) => arg === '--exclude' ? [args[index + 1]] : []).filter(Boolean); const result = await scan(root, { target: value('--target'), failOn: value('--fail-on'), baseRef: value('--base-ref'), exclude: excludes.length ? excludes : undefined }); const format = value('--format') ?? 'human';
19
20
  console.log(format === 'json' ? JSON.stringify(result, null, 2) : format === 'sarif' ? JSON.stringify(toSarif(result), null, 2) : formatHuman(result));
20
- process.exitCode = shouldFail(result, result.failOn) ? 1 : 0;
21
+ process.exitCode = shouldFail(result, result.failOn, args.includes('--fail-on-new')) ? 1 : 0;
21
22
  } catch (error) { console.error(`shopify-upgrade-guard: ${error.message}`); process.exitCode = 2; }
package/src/output.js CHANGED
@@ -12,7 +12,7 @@ export function formatHuman(result) {
12
12
  return lines.join('\n');
13
13
  }
14
14
  function sectionLines(findings, fallback) { if (!findings.length) return [`No ${fallback.toLowerCase()}.`]; const lines = []; for (const finding of [...findings].sort((a, b) => order[a.severity] - order[b.severity])) lines.push(`${finding.severity.toUpperCase()} ${finding.ruleId}`, finding.title, ` ${finding.file}:${finding.line}`, ` ${finding.reason ?? finding.currentState}`, ` Migration: ${finding.migration}`, ` Source: ${finding.source}`, ''); return lines; }
15
- export function shouldFail(result, threshold = 'never') { if (threshold === 'never') return false; return activeFindings(result).some((finding) => threshold === 'info' || (threshold === 'warning' && finding.severity !== 'info') || (threshold === 'error' && finding.severity === 'error')); }
15
+ export function shouldFail(result, threshold = 'never', newOnly = false) { if (threshold === 'never') return false; return activeFindings(result).filter((finding) => !newOnly || finding.lifecycle === 'NEW').some((finding) => threshold === 'info' || (threshold === 'warning' && finding.severity !== 'info') || (threshold === 'error' && finding.severity === 'error')); }
16
16
  export function toSarif(result) {
17
17
  const ruleMap = new Map(); for (const finding of activeFindings(result)) if (!ruleMap.has(finding.ruleId)) ruleMap.set(finding.ruleId, { id: finding.ruleId, shortDescription: { text: finding.title }, helpUri: finding.source, properties: { severity: finding.severity, confidence: finding.confidence } });
18
- const rules = [...ruleMap.values()]; return { version: '2.1.0', $schema: 'https://json.schemastore.org/sarif-2.1.0.json', runs: [{ tool: { driver: { name: 'Shopify Upgrade Guard', informationUri: 'https://github.com/efegokdemir/shopify-upgrade-guard', rules } }, results: activeFindings(result).map((finding) => ({ ruleId: finding.ruleId, ruleIndex: rules.findIndex((rule) => rule.id === finding.ruleId), level: finding.severity === 'error' ? 'error' : finding.severity === 'warning' ? 'warning' : 'note', message: { text: `${finding.title}. ${finding.reason ?? finding.migration}` }, locations: [{ physicalLocation: { artifactLocation: { uri: finding.file }, region: { startLine: finding.line, startColumn: finding.column } } }] })) }] }; }
18
+ const rules = [...ruleMap.values()]; return { version: '2.1.0', $schema: 'https://json.schemastore.org/sarif-2.1.0.json', runs: [{ tool: { driver: { name: 'Shopify Upgrade Guard', informationUri: 'https://github.com/efegokdemir/shopify-upgrade-guard', rules } }, results: activeFindings(result).map((finding) => ({ ruleId: finding.ruleId, ruleIndex: rules.findIndex((rule) => rule.id === finding.ruleId), level: finding.severity === 'error' ? 'error' : finding.severity === 'warning' ? 'warning' : 'note', message: { text: `${finding.title}. ${finding.reason ?? finding.migration}` }, partialFingerprints: { primaryLocationLineHash: finding.fingerprint }, locations: [{ physicalLocation: { artifactLocation: { uri: finding.file.replaceAll('\\', '/') }, region: { startLine: finding.line, startColumn: finding.column } } }] })) }] }; }
package/src/rules.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import { isAtOrAfter, statusFor, latestStable, becomesUnsupportedBefore } from './versions.js';
2
2
 
3
3
  const checkoutDeprecation = '2026-07';
4
+ const removalVersion = '2026-10';
5
+ const sourceExtensions = /\.(?:[jt]sx?|graphql|gql)$/;
4
6
  export const rules = [
5
7
  {
6
8
  id: 'UG-CHECKOUT-001', surface: 'checkout_ui_extension', severity: 'warning', title: 'Buyer journey intercept is deprecated', deprecatedIn: checkoutDeprecation, confidence: 'high',
7
9
  documentationUrl: 'https://shopify.dev/changelog/deprecating-the-usebuyerjourneyintercept-api-on-checkout-ui-extensions', migrationUrl: 'https://shopify.dev/changelog/deprecating-the-usebuyerjourneyintercept-api-on-checkout-ui-extensions',
8
10
  description: 'useBuyerJourneyIntercept and buyerJourney.intercept are deprecated from Checkout UI extension version 2026-07. Migrate validation to a cart and checkout validation Function.',
9
- detect(file) { if (!/\.(?:[jt]sx?|graphql|gql)$/.test(file.relativePath)) return []; return matches(maskCommentsAndStrings(file.text), /\b(?:useBuyerJourneyIntercept|buyerJourney\.intercept)\b/g, 'Replace client-side blocking with a cart and checkout validation Function.'); },
11
+ detect(file) { if (!sourceExtensions.test(file.relativePath)) return []; return matches(maskCommentsAndStrings(file.text), /\b(?:useBuyerJourneyIntercept|buyerJourney\.intercept)\b/g, 'Replace client-side blocking with a cart and checkout validation Function.'); },
10
12
  evaluate(match, context) { return versionAware(match, this, context); }
11
13
  },
12
14
  {
@@ -16,6 +18,34 @@ export const rules = [
16
18
  detect(file) { if (!file.relativePath.endsWith('shopify.extension.toml')) return []; return matches(maskTomlComments(file.text), /^\s*block_progress\s*=\s*(?:true|false)\s*$/gm, 'Move validation logic to a cart and checkout validation Function.'); },
17
19
  evaluate(match, context) { return versionAware(match, this, context); }
18
20
  },
21
+ {
22
+ id: 'UG-CUSTOMER-001', surface: 'customer_account_api', severity: 'error', title: 'Customer Account checkout types are removed', removedIn: removalVersion, confidence: 'high',
23
+ documentationUrl: 'https://shopify.dev/changelog/customer-account-api-last-incomplete-checkout-and-checkout-types-removed', migrationUrl: 'https://shopify.dev/changelog/customer-account-api-last-incomplete-checkout-and-checkout-types-removed',
24
+ description: 'Customer.lastIncompleteCheckout and the Checkout type subtree are removed in API version 2026-10 with no replacement. Use Storefront cart flows or Customer.orders as appropriate.',
25
+ detect(file) { if (!sourceExtensions.test(file.relativePath)) return []; return matches(maskComments(file.text), /\blastIncompleteCheckout\b|\bCheckout\s*\{/g, 'Remove the Customer Account checkout field/type usage and migrate to Storefront cart flows or Customer.orders.'); },
26
+ evaluate(match, context) { return removalAware(match, this, context); }
27
+ },
28
+ {
29
+ id: 'UG-POS-001', surface: 'pos_ui_extension', severity: 'error', title: 'POS currentSession.staffMemberId is removed', removedIn: removalVersion, confidence: 'high',
30
+ documentationUrl: 'https://shopify.dev/changelog/removed-session-currentsession-staffmemberid-from-pos-ui-extensions-2026-10', migrationUrl: 'https://shopify.dev/changelog/removed-session-currentsession-staffmemberid-from-pos-ui-extensions-2026-10',
31
+ description: 'session.currentSession.staffMemberId is removed from POS UI Extensions in API version 2026-10. Use session.staffMember.value?.id or subscribe to staffMember.',
32
+ detect(file) { if (!/\.(?:[jt]sx?)$/.test(file.relativePath)) return []; return matches(maskComments(file.text), /\bsession\.currentSession\.staffMemberId\b/g, 'Use session.staffMember.value?.id or subscribe to the staffMember signal.'); },
33
+ evaluate(match, context) { return removalAware(match, this, context); }
34
+ },
35
+ {
36
+ id: 'UG-ADMIN-001', surface: 'admin_graphql_api', severity: 'warning', title: 'Legacy GraphQL priceRule field is removed', removedIn: removalVersion, confidence: 'medium',
37
+ documentationUrl: 'https://shopify.dev/release-notes/2026-10', migrationUrl: 'https://shopify.dev/release-notes/2026-10',
38
+ description: 'Legacy priceRule fields and types are removed from the Admin GraphQL API in 2026-10. Use discountTitle or discountCode where applicable.',
39
+ detect(file) { if (!sourceExtensions.test(file.relativePath)) return []; return matches(maskComments(file.text), /\bpriceRule\b/g, 'Replace priceRule usage with the supported discountTitle or discountCode fields.'); },
40
+ evaluate(match, context) { return removalAware(match, this, context); }
41
+ },
42
+ {
43
+ id: 'UG-SCRIPT-001', surface: 'online_store_script_tags', severity: 'error', title: 'Script Tag creation and updates are deprecated', deprecatedIn: removalVersion, confidence: 'high',
44
+ documentationUrl: 'https://shopify.dev/changelog/online-store-script-tags-deprecation', migrationUrl: 'https://shopify.dev/docs/apps/build/online-store/script-tag-deprecation',
45
+ description: 'ScriptTag create/update operations error from 2026-10 and storefront injection stops in 2027-03. Migrate to a theme app extension/app embed block or web pixel.',
46
+ detect(file) { if (!sourceExtensions.test(file.relativePath)) return []; return matches(maskComments(file.text), /\b(?:scriptTagCreate|scriptTagUpdate|script_tags)\b/g, 'Migrate Script Tag injection to a theme app extension/app embed block or web pixel.'); },
47
+ evaluate(match, context) { return removalAware(match, this, context); }
48
+ },
19
49
  {
20
50
  id: 'UG-REST-001', surface: 'admin_rest_api', severity: 'warning', title: 'REST Admin API usage is legacy', confidence: 'high',
21
51
  documentationUrl: 'https://shopify.dev/docs/api/admin-rest', migrationUrl: 'https://shopify.dev/docs/api/admin-graphql',
@@ -45,7 +75,12 @@ function versionAware(match, rule, context) {
45
75
  if (!currentAffected && !targetAffected) return null;
46
76
  return { ...match, classification: currentAffected ? 'current' : 'target', reason: currentAffected ? rule.description : `This API becomes deprecated before target ${context.targetVersion}. ${rule.description}` };
47
77
  }
78
+ function removalAware(match, rule, context) {
79
+ if (!isAtOrAfter(context.targetVersion, rule.removedIn ?? rule.deprecatedIn)) return null;
80
+ return { ...match, classification: 'target', reason: `This usage is affected by target ${context.targetVersion}. ${rule.description}` };
81
+ }
48
82
  function matches(text, pattern, guidance) { return [...text.matchAll(pattern)].map((match) => { const before = text.slice(0, match.index); return { file: null, line: before.split('\n').length, column: match.index - before.lastIndexOf('\n'), snippet: match[0], guidance }; }); }
49
83
  export function attachFile(matchesForFile, file) { return matchesForFile.map((match) => ({ ...match, file: file.relativePath })); }
50
84
  function maskCommentsAndStrings(text) { return text.replace(/\/\/[^\n]*|\/\*[\s\S]*?\*\/|(['"`])(?:\\.|(?!\1)[^\\])*\1/g, (value) => value.replace(/[^\n]/g, ' ')); }
85
+ function maskComments(text) { return text.replace(/\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (value) => value.replace(/[^\n]/g, ' ')); }
51
86
  function maskTomlComments(text) { return text.replace(/#[^\n]*/g, (value) => value.replace(/[^\n]/g, ' ')); }
package/src/scan.js CHANGED
@@ -10,7 +10,7 @@ export async function loadConfig(root) {
10
10
  try { const config = JSON.parse(await readFile(join(root, '.upgradeguard.json'), 'utf8')); validateConfig(config); return config; }
11
11
  catch (error) { if (error.code === 'ENOENT') return {}; throw new Error(`Invalid .upgradeguard.json: ${error.message}`); }
12
12
  }
13
- function validateConfig(config) { if (config.targetVersion !== undefined && !parseVersion(config.targetVersion)) throw new Error('targetVersion must be a quarterly YYYY-MM version.'); if (config.failOn !== undefined && !['never', 'error', 'warning', 'info'].includes(config.failOn)) throw new Error('failOn must be never, error, warning, or info.'); if (config.exclude !== undefined && (!Array.isArray(config.exclude) || config.exclude.some((pattern) => validateExcludePattern(pattern) === undefined))) throw new Error('exclude must be an array of repository-relative glob patterns.'); }
13
+ function validateConfig(config) { if (config.targetVersion !== undefined && !parseVersion(config.targetVersion)) throw new Error('targetVersion must be a quarterly YYYY-MM version.'); if (config.failOn !== undefined && !['never', 'error', 'warning', 'info'].includes(config.failOn)) throw new Error('failOn must be never, error, warning, or info.'); if (config.exclude !== undefined && (!Array.isArray(config.exclude) || config.exclude.some((pattern) => validateExcludePattern(pattern) === undefined))) throw new Error('exclude must be an array of repository-relative glob patterns.'); if (config.baseline !== undefined && (typeof config.baseline !== 'string' || config.baseline.startsWith('/') || config.baseline.includes('..'))) throw new Error('baseline must be a repository-relative file path.'); }
14
14
  export async function scan(root, options = {}) {
15
15
  const config = await loadConfig(root); const targetVersion = options.target ?? config.targetVersion ?? latestStable.version; const failOn = options.failOn ?? config.failOn ?? 'never'; targetRecord(targetVersion);
16
16
  const exclude = options.exclude ?? config.exclude ?? []; exclude.forEach(validateExcludePattern);
@@ -20,11 +20,12 @@ export async function scan(root, options = {}) {
20
20
  const detected = rule.detect(file); const matches = detected.length ? attachFile(detected, file) : inventoryMatches(rule, inventory, file);
21
21
  for (const match of matches) { const evaluated = rule.evaluate(match, context); if (evaluated) findings.push(toFinding(rule, evaluated, targetVersion)); }
22
22
  }
23
- const baseline = await loadBaseline(root); let resultFindings = applyBaseline(dedupe(findings), baseline);
23
+ const baseline = options.noBaseline ? { fingerprints: new Set(), entries: [], file: null } : await loadBaseline(root, options.baseline ?? config.baseline); let resultFindings = applyBaseline(dedupe(findings), baseline);
24
24
  const baseRef = options.baseRef ?? process.env.GITHUB_BASE_REF;
25
- if (baseRef) { try { const ranges = await changedLines(root, baseRef); resultFindings = resultFindings.map((finding) => ({ ...finding, changeStatus: isChanged(finding, ranges) ? 'introduced' : 'pre-existing' })); } catch { /* Local scans may not have the PR base available. */ } }
25
+ if (baseRef) { try { const ranges = await changedLines(root, baseRef); resultFindings = resultFindings.map((finding) => ({ ...finding, changeStatus: isChanged(finding, ranges) ? 'introduced' : 'pre-existing', lifecycle: isChanged(finding, ranges) ? 'NEW' : 'EXISTING' })); } catch { resultFindings = resultFindings.map((finding) => ({ ...finding, lifecycle: finding.baselined ? 'EXISTING' : 'UNCHANGED' })); } }
26
+ else resultFindings = resultFindings.map((finding) => ({ ...finding, lifecycle: finding.baselined ? 'EXISTING' : 'UNCHANGED' }));
26
27
  const active = resultFindings.filter((finding) => !finding.baselined); const currentProblems = active.filter((finding) => finding.classification === 'current'); const upgradeBlockers = active.filter((finding) => finding.classification === 'target');
27
- return { schemaVersion: 1, tool: 'shopify-upgrade-guard', targetVersion, failOn, latestStable: latestStable.version, filesScanned: files.length, inventory, findings: resultFindings, currentProblems, upgradeBlockers, baselinedCount: resultFindings.filter((finding) => finding.baselined).length, resolvedBaselineCount: [...baseline.fingerprints].filter((fingerprint) => !resultFindings.some((finding) => finding.fingerprint === fingerprint)).length };
28
+ return { schemaVersion: 1, tool: 'shopify-upgrade-guard', targetVersion, failOn, latestStable: latestStable.version, filesScanned: files.length, inventory, findings: resultFindings, currentProblems, upgradeBlockers, baselinedCount: resultFindings.filter((finding) => finding.baselined).length, resolvedBaselineCount: [...baseline.fingerprints].filter((fingerprint) => !resultFindings.some((finding) => finding.fingerprint === fingerprint)).length, baselineFile: baseline.file };
28
29
  }
29
30
  function inventoryMatches(rule, inventory, file) { if (!rule.id.startsWith('UG-VERSION')) return []; return inventory.filter((item) => item.file === file.relativePath).map((item) => ({ file: item.file, line: item.line, column: 1, snippet: `${item.source}: ${item.version}`, version: item.version, guidance: 'Update this Shopify version to a supported stable release and test the migration.' })); }
30
31
  function toFinding(rule, match, targetVersion) { return { ruleId: rule.id, severity: rule.severity, title: rule.title, surface: rule.surface, file: match.file, line: match.line, column: match.column, snippet: match.snippet, targetVersion, currentState: rule.description, migration: match.guidance, source: rule.documentationUrl, migrationUrl: rule.migrationUrl ?? null, confidence: rule.confidence, classification: match.classification, reason: match.reason, deprecatedIn: rule.deprecatedIn ?? null, removedIn: rule.removedIn ?? null }; }