Skip to content

Commit

Permalink
Add redirects/rewrite self-serve config (#358)
Browse files Browse the repository at this point in the history
  • Loading branch information
rezrah authored Apr 6, 2023
1 parent d5de594 commit 60d89db
Show file tree
Hide file tree
Showing 7 changed files with 2,584 additions and 2,608 deletions.
8 changes: 6 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@ jobs:
with:
node-version: '14.x'

- name: install and build
- name: Install and build
run: |
yarn
yarn build
- name: Copy rewrites to server root
- name: Build redirects
run: |
node ./script/redirects.js
- name: Copy redirects to server root
run: |
cp web.config public/
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/deploy_staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ jobs:
with:
node-version: '14.x'

- name: install and build
- name: Install and build
run: |
yarn
yarn build
- name: Build redirects
run: |
node ./script/redirects.js
- name: Copy rewrites to server root
run: |
cp web.config public/
Expand Down
44 changes: 30 additions & 14 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,40 @@

1. Get [Yarn](https://yarnpkg.com).

```sh
npm i -g yarn@latest
```
```sh
npm i -g yarn@latest
```

**Why Yarn?** We use [selective resolution
overrides](https://yarnpkg.com/lang/en/docs/selective-version-resolutions/)
to work around React version discrepancies in several of our dependencies.
**Why Yarn?** We use [selective resolution
overrides](https://yarnpkg.com/lang/en/docs/selective-version-resolutions/)
to work around React version discrepancies in several of our dependencies.

1. Install dependencies:

```sh
yarn
```
```sh
yarn
```

1. Run the development server:

```sh
yarn start
# npm works, too!
npm start
```
```sh
yarn start
# npm works, too!
npm start
```

## Redirects

When adding redirects to the [`redirects.json`](./redirects.json) file, it's important to follow the correct schema format to ensure that the redirects (and rewrites) work as intended.

Each redirect should be added as an object with three required properties: `name`, `match`, and `destination`.

- The `name` property should be a descriptive name for the redirect
- The `match` property should be a regular expression that matches the URL pattern to be redirected.
- The `destination` property should be the URL to which the matched pattern should be redirected.

It's important to test the redirects in our `staging` environment prior to a production deployment to ensure that they work correctly and do not cause any unintended downtime.

Please refer to the following diagram for more details around deployment:

![Image showing a flow diagram, asking the reader whether they have previously tested in a staging environment. If not they should merge changes to the staging branch first, before merging to main](https://user-images.githubusercontent.com/13340707/227264916-85df29da-735d-43d0-baa7-5e3c0e8b6b6f.png)
131 changes: 131 additions & 0 deletions redirects.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
{
"redirects": [
{
"name": "Redirect /design/accessibility",
"match": "(design)/(accessibility)(.*)",
"destination": "/design/guides/accessibility{R:3}"
},
{
"name": "Redirect /octicons/packages/ruby",
"match": "octicons/packages/ruby",
"destination": "https://github.com/primer/octicons/tree/main/lib/octicons_gem#readme"
},
{
"name": "Redirect /octicons/packages/jekyll",
"match": "octicons/packages/jekyll",
"destination": "https://github.com/primer/octicons/tree/main/lib/octicons_jekyll#readme"
},
{
"name": "Redirect /octicons/packages/javascript",
"match": "octicons/packages/javascript",
"destination": "https://github.com/primer/octicons/tree/main/lib/octicons_node#readme"
},
{
"name": "Redirect /octicons/packages/react",
"match": "octicons/packages/react",
"destination": "https://github.com/primer/octicons/tree/main/lib/octicons_react#readme"
},
{
"name": "Redirect /octicons/packages/styled-system",
"match": "octicons/packages/styled-system",
"destination": "https://github.com/primer/octicons/tree/main/lib/octicons_styled#readme"
},
{
"name": "Redirect /octicons/guidelines/design",
"match": "octicons/guidelines/design",
"destination": "/design/foundations/icons/design-guidelines"
},
{
"name": "Redirect /octicons/*",
"match": "(octicons)(.*)",
"destination": "/design/foundations/icons{R:2}"
}
],
"rewrites": [
{
"name": "ViewComponents Preview assets proxy",
"match": "^assets/(.*)",
"destination": "https://view-components-storybook.eastus.cloudapp.azure.com/assets/{R:1}"
},
{
"name": "ViewComponents Lookbook assets proxy",
"match": "^lookbook-assets/(.*)",
"destination": "https://view-components-storybook.eastus.cloudapp.azure.com/lookbook-assets/{R:1}"
},
{
"name": "ViewComponents Lookbook proxy",
"match": "^view-components/lookbook/(.*)",
"destination": "https://view-components-storybook.eastus.cloudapp.azure.com/view-components/lookbook/{R:1}"
},
{
"name": "ViewComponents Rails App proxy",
"match": "^view-components/rails-app/(.*)",
"destination": "https://view-components-storybook.eastus.cloudapp.azure.com/view-components/rails-app/{R:1}"
},
{
"name": "ViewComponents proxy",
"match": "^view-components/(.*)",
"destination": "https://primer.github.io/view_components/{R:1}"
},
{
"name": "Design proxy",
"match": "^design/(.*)",
"destination": "https://primer.github.io/design/{R:1}"
},
{
"name": "Presentations proxy",
"match": "^presentations/(.*)",
"destination": "https://primer.github.io/presentations/{R:1}"
},
{
"name": "Doctocat",
"match": "^doctocat/(.*)",
"destination": "https://primer.github.io/doctocat/{R:1}"
},
{
"name": "CLI proxy",
"match": "^cli/(.*)",
"destination": "https://primer.github.io/cli/{R:1}"
},
{
"name": "Primitives proxy",
"match": "^primitives/(.*)",
"destination": "https://primer.github.io/primitives/{R:1}"
},
{
"name": "Mobile proxy",
"match": "^mobile/(.*)",
"destination": "https://primer.github.io/mobile/{R:1}"
},
{
"name": "Brand proxy",
"match": "^brand/(.*)",
"destination": "https://primer.github.io/brand/{R:1}"
},
{
"name": "Desktop proxy",
"match": "^desktop/(.*)",
"destination": "https://primer.github.io/desktop/{R:1}"
},
{
"name": "Contribute proxy",
"match": "^contribute/(.*)",
"destination": "https://primer.github.io/contribute/{R:1}"
},
{
"name": "Prism proxy",
"match": "^prism/(.*)",
"destination": "https://primer.github.io/prism/{R:1}"
},
{
"name": "React reverse-proxy",
"match": "^react/(.*)",
"destination": "https://primer.github.io/react/{R:1}"
},
{
"name": "CSS reverse-proxy",
"match": "^css/(.*)",
"destination": "https://primer.github.io/css/{R:1}"
}
]
}
169 changes: 169 additions & 0 deletions script/redirects.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
const fs = require('fs')
const path = require('path')

function validator(input) {
if (!input || typeof input !== "object") {
return { valid: false, error: "Input must be an object" };
}

if (!Array.isArray(input.redirects) || !Array.isArray(input.rewrites)) {
return { valid: false, error: "Input must have 'redirects' and 'rewrites' arrays" };
}

for (const redirect of input.redirects) {
if (
typeof redirect !== "object" ||
typeof redirect.name !== "string" ||
typeof redirect.match !== "string" ||
typeof redirect.destination !== "string"
) {
const invalidKey = Object.keys(redirect).find(
(key) =>
typeof redirect[key] !== "string" ||
(key !== "name" && key !== "match" && key !== "destination")
) || "";
return { valid: false, error: `'${invalidKey}' redirect has invalid value ` };
}

try {
new RegExp(redirect.match);
} catch (error) {
return { valid: false, error: `Redirect has invalid regex in 'match' property` };
}
}

for (const rewrite of input.rewrites) {
if (
typeof rewrite !== "object" ||
typeof rewrite.name !== "string" ||
typeof rewrite.match !== "string" ||
typeof rewrite.destination !== "string"
) {
const invalidKey = Object.keys(rewrite).find(
(key) =>
typeof rewrite[key] !== "string" ||
(key !== "name" && key !== "match" && key !== "destination")
) || "";
return { valid: false, error: `'${invalidKey}' rewrite has an invalid value ` };
}

try {
new RegExp(rewrite.match);
} catch (error) {
return { valid: false, error: `Rewrite has invalid regex in 'match' property` };
}
}

return { valid: true };
}

function buildRedirects() {
const inputFile = path.join(__dirname, '../redirects.json')
const outputFile = path.join(__dirname, '../web.config')

const input = JSON.parse(fs.readFileSync(inputFile))

const {valid, error} = validator(input)


if (!valid) {
throw new Error(`Invalid redirects.json file. ${error}`)
}

console.info(`redirects.json was validated successfully` )
console.info('...')

const redirects = input.redirects
.map(
({name, match, destination}) => `<rule name="${name}" stopProcessing="true">
<match url="${match}" />
<action type="Redirect" redirectType="Permanent" url="${destination}" />
</rule>
`
)
.join('')

const rewrites = input.rewrites
.map(
({name, match, destination}) => `<rule name="${name}" stopProcessing="true">
<match url="${match}" ignoreCase="true" />
<conditions>
<add input="{HTTP_HOST}" pattern="^(?:www.)?(.*)$" />
</conditions>
<action type="Rewrite" url="${destination}" />
<serverVariables>
<set name="HTTP_X_UNPROXIED_URL" value="${destination}" />
<set name="HTTP_X_ORIGINAL_ACCEPT_ENCODING" value="{HTTP_ACCEPT_ENCODING}" />
<set name="HTTP_X_ORIGINAL_HOST" value="{HTTP_HOST}" />
<set name="HTTP_ACCEPT_ENCODING" value="" />
</serverVariables>
</rule>
`
)
.join('')

const config = `<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<staticContent>
<mimeMap fileExtension=".json" mimeType="application/json" />
<mimeMap fileExtension=".webmanifest" mimeType="application/manifest+json" />
<remove fileExtension=".woff2" />
<mimeMap fileExtension=".woff2" mimeType="font/woff2" />
</staticContent>
<httpErrors errorMode="Custom" existingResponse="Auto" defaultResponseMode="ExecuteURL" >
<remove statusCode="404"/>
<error statusCode="404" responseMode="ExecuteURL" path="/404/index.html" />
</httpErrors>
<rewrite>
<rules>
<!--BEGIN SSL-->
<rule name="ForceSSL" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="^OFF$" ignoreCase="true" />
</conditions>
<action type="Redirect" url="https://{C:2}/{R:1}" redirectType="Permanent" />
</rule>
<!--END SSL-->
<!--BEGIN Trailing slash enforcement-->
<rule name="Add trailing slash" stopProcessing="true">
<match url="(.*[^/])$" />
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
<add input="{REQUEST_FILENAME}" pattern="(.*?)\\.[a-zA-Z]{1,4}$" negate="true" />
<add input="{URL}" negate="true" pattern="\\.woff2$" />
<add input="{URL}" negate="true" pattern="\\.webmanifest$" />
</conditions>
<action type="Redirect" redirectType="Temporary" url="{R:1}/" />
</rule>
<!--END Trailing slash enforcement-->
<!--BEGIN 301 redirects. Goes before URL rewrites -->
${redirects}
<!--END 301 redirects -->
${rewrites}
</rules>
<outboundRules>
<preConditions>
<preCondition name="CheckContentType">
<add input="{RESPONSE_CONTENT_TYPE}" pattern="^(text/html|text/plain|text/xml|application/rss\\+xml)" />
</preCondition>
</preConditions>
</outboundRules>
</rewrite>
</system.webServer>
</configuration>`


fs.writeFileSync(outputFile, config)
console.info(`Redirects built successfully and written to ${outputFile}`)
}

try {
console.info('Building redirects...')
console.info('...')
buildRedirects()
} catch (error) {
console.error(error)
}
Loading

0 comments on commit 60d89db

Please sign in to comment.