├── compose.yaml ├── .github └── workflows │ └── tests.yml ├── LICENSE ├── plugin.yml ├── hooks └── post-command ├── examples └── pipeline.yml ├── tests └── test.sh └── README.md /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | tests: 3 | image: "buildkite/plugin-tester" 4 | volumes: 5 | - ".:/plugin:ro" 6 | lint: 7 | image: "buildkite/plugin-linter" 8 | command: ["--id", "envato/aws-s3-website-redirect"] 9 | volumes: 10 | - ".:/plugin:ro" 11 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: [push, pull_request] 3 | jobs: 4 | plugin-tests: 5 | name: Tests 6 | runs-on: ubuntu-latest 7 | container: 8 | image: buildkite/plugin-tester:latest 9 | volumes: 10 | - "${{github.workspace}}:/plugin" 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: tests 14 | run: bats tests/ 15 | plugin-lint: 16 | name: Lint 17 | runs-on: ubuntu-latest 18 | container: 19 | image: buildkite/plugin-linter:latest 20 | volumes: 21 | - "${{github.workspace}}:/plugin" 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: lint 25 | run: lint --id envato/aws-s3-website-redirect 26 | plugin-shellcheck: 27 | name: Shellcheck 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v4 31 | - name: Run ShellCheck 32 | uses: ludeeus/action-shellcheck@1.1.0 33 | with: 34 | check_together: 'yes' 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Envato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /plugin.yml: -------------------------------------------------------------------------------- 1 | name: aws-s3-website-redirect 2 | description: Add website redirects via S3 by setting x-amz-website-redirect-location metadata 3 | author: https://github.com/envato 4 | requirements: 5 | - aws-cli 6 | 7 | configuration: 8 | properties: 9 | redirects: 10 | type: array 11 | items: 12 | type: object 13 | properties: 14 | source: 15 | type: string 16 | description: The S3 path to redirect from (e.g., "my-old-docs/") 17 | destination: 18 | type: string 19 | description: The URL to redirect to (e.g., "https://example.com/my-new-docs/") 20 | required: 21 | - source 22 | - destination 23 | bucket: 24 | type: string 25 | description: The S3 bucket name (e.g., "example.com") 26 | region: 27 | type: string 28 | description: The AWS region for the S3 bucket 29 | default: us-east-1 30 | source: 31 | type: string 32 | description: The S3 path to redirect from (single redirect mode) 33 | destination: 34 | type: string 35 | description: The URL to redirect to (single redirect mode) 36 | additionalProperties: false 37 | oneOf: 38 | - required: 39 | - redirects 40 | - bucket 41 | - required: 42 | - source 43 | - destination 44 | - bucket -------------------------------------------------------------------------------- /hooks/post-command: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | echo "--- :s3: Setting up S3 website redirects" 5 | 6 | # Get plugin configuration 7 | BUCKET="${BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_BUCKET:-}" 8 | REGION="${BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_REGION:-us-east-1}" 9 | 10 | 11 | # Validate bucket is provided 12 | if [[ -z "${BUCKET}" ]]; then 13 | echo "❌ Error: 'bucket' parameter is required" 14 | exit 1 15 | fi 16 | 17 | # Build AWS CLI options 18 | AWS_OPTS="--region ${REGION}" 19 | 20 | # Function to create a redirect 21 | create_redirect() { 22 | local source="$1" 23 | local destination="$2" 24 | 25 | # Ensure source doesn't start with s3:// or bucket name 26 | source="${source#s3://}" 27 | source="${source#"${BUCKET}"/}" 28 | 29 | # Construct full S3 path 30 | local s3_path="s3://${BUCKET}/${source}" 31 | 32 | echo "Creating redirect: ${source} → ${destination}" 33 | 34 | # Use the aws s3 cp command with --website-redirect to create the redirect 35 | # shellcheck disable=SC2086 36 | if aws s3 cp ${AWS_OPTS} --website-redirect "${destination}" - "${s3_path}" <<< ""; then 37 | echo "✅ Successfully created redirect from ${source} to ${destination}" 38 | else 39 | echo "❌ Failed to create redirect from ${source} to ${destination}" 40 | exit 1 41 | fi 42 | } 43 | 44 | # Check if single redirect mode or array mode 45 | if [[ -n "${BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_SOURCE:-}" ]] && \ 46 | [[ -n "${BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_DESTINATION:-}" ]]; then 47 | # Single redirect mode 48 | echo "Processing single redirect..." 49 | SOURCE="${BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_SOURCE}" 50 | DESTINATION="${BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_DESTINATION}" 51 | create_redirect "${SOURCE}" "${DESTINATION}" 52 | else 53 | # Array mode - iterate through redirects 54 | echo "Processing multiple redirects..." 55 | redirect_count=0 56 | 57 | while IFS= read -r var; do 58 | if [[ "${var}" =~ ^BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_REDIRECTS_([0-9]+)_SOURCE= ]]; then 59 | index="${BASH_REMATCH[1]}" 60 | 61 | # Get source and destination for this index 62 | source_var="BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_REDIRECTS_${index}_SOURCE" 63 | dest_var="BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_REDIRECTS_${index}_DESTINATION" 64 | 65 | source="${!source_var:-}" 66 | destination="${!dest_var:-}" 67 | 68 | if [[ -n "${source}" ]] && [[ -n "${destination}" ]]; then 69 | create_redirect "${source}" "${destination}" 70 | redirect_count=$((redirect_count + 1)) 71 | fi 72 | fi 73 | done < <(env | grep "^BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_REDIRECTS_" | sort) 74 | 75 | if [[ ${redirect_count} -eq 0 ]]; then 76 | echo "⚠️ Warning: No redirects were configured" 77 | else 78 | echo "✅ Successfully created ${redirect_count} redirect(s)" 79 | fi 80 | fi 81 | 82 | echo "--- :white_check_mark: S3 website redirects complete" -------------------------------------------------------------------------------- /examples/pipeline.yml: -------------------------------------------------------------------------------- 1 | # Example pipeline showing how to use the s3-website-redirect plugin 2 | 3 | agents: 4 | queue: platform-x86-large 5 | 6 | steps: 7 | # Example 1: Single redirect after publishing docs 8 | - label: ":books: Publish Docs with Single Redirect" 9 | command: 10 | - "yarn install" 11 | - "yarn docs build" 12 | plugins: 13 | - envato/aws-assume-role#v0.2.0: 14 | role: arn:aws:iam::123456789012:role/docs-role 15 | - docker#v5.3.0: 16 | image: node:18-alpine 17 | environment: 18 | - CI 19 | - envato/aws-s3-sync#v0.5.0: 20 | source: docs/.vitepress/dist/ 21 | destination: s3://example.com/my-project/ 22 | delete: true 23 | # Add a single redirect 24 | - s3-website-redirect: 25 | bucket: example.com 26 | source: my-project/old-getting-started/ 27 | destination: https://example.com/my-project/getting-started/ 28 | 29 | # Example 2: Multiple redirects 30 | - label: ":books: Publish Docs with Multiple Redirects" 31 | command: 32 | - "yarn install" 33 | - "yarn docs build" 34 | plugins: 35 | - envato/aws-assume-role#v0.2.0: 36 | role: arn:aws:iam::123456789012:role/docs-role 37 | - docker#v5.3.0: 38 | image: node:18-alpine 39 | - envato/aws-s3-sync#v0.5.0: 40 | source: docs/.vitepress/dist/ 41 | destination: s3://example.com/my-project/ 42 | delete: true 43 | # Add multiple redirects at once 44 | - s3-website-redirect: 45 | bucket: example.com 46 | region: us-east-1 47 | redirects: 48 | # Redirect old CLI docs to new location 49 | - source: my-project/cli/ 50 | destination: https://example.com/my-project/reference/cli/ 51 | # Redirect deprecated API docs 52 | - source: my-project/api/v1/ 53 | destination: https://example.com/my-project/api/v2/ 54 | # Redirect old tutorial 55 | - source: my-project/tutorials/basic-setup/ 56 | destination: https://example.com/my-project/getting-started/ 57 | # Redirect renamed section 58 | - source: my-project/configuration/ 59 | destination: https://example.com/my-project/reference/configuration/ 60 | 61 | # Example 3: Redirects with custom region 62 | - label: ":books: Publish to Different Region" 63 | command: 64 | - "yarn docs build" 65 | plugins: 66 | - s3-website-redirect: 67 | bucket: my-docs-bucket 68 | region: ap-southeast-2 69 | redirects: 70 | - source: old-path/ 71 | destination: https://example.com/new-path/ 72 | - source: legacy/ 73 | destination: https://example.com/current/ 74 | 75 | # Example 4: Standalone redirect step (without uploading new content) 76 | - label: ":redirect: Add Documentation Redirects" 77 | command: "echo 'Adding redirects only'" 78 | plugins: 79 | - envato/aws-assume-role#v0.2.0: 80 | role: arn:aws:iam::123456789012:role/docs-role 81 | - s3-website-redirect: 82 | bucket: example.com 83 | redirects: 84 | - source: my-project/deprecated-feature/ 85 | destination: https://example.com/my-project/ 86 | - source: my-project/moved-page.html 87 | destination: https://example.com/my-project/new-location.html -------------------------------------------------------------------------------- /tests/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Test script for s3-website-redirect plugin 3 | 4 | # Colors for output 5 | RED='\033[0;31m' 6 | GREEN='\033[0;32m' 7 | YELLOW='\033[1;33m' 8 | NC='\033[0m' # No Color 9 | 10 | TESTS_PASSED=0 11 | TESTS_FAILED=0 12 | 13 | # Helper functions 14 | print_test() { 15 | echo "" 16 | echo -e "${YELLOW}TEST: $1${NC}" 17 | } 18 | 19 | pass() { 20 | echo -e "${GREEN}✓ PASS${NC}: $1" 21 | TESTS_PASSED=$((TESTS_PASSED + 1)) 22 | } 23 | 24 | fail() { 25 | echo -e "${RED}✗ FAIL${NC}: $1" 26 | TESTS_FAILED=$((TESTS_FAILED + 1)) 27 | } 28 | 29 | cleanup_env() { 30 | for var in $(env | grep "^BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT" | cut -d= -f1); do 31 | unset "$var" 2>/dev/null || true 32 | done 33 | } 34 | 35 | # Test 1: Single redirect configuration 36 | test_single_redirect() { 37 | print_test "Single redirect configuration" 38 | 39 | export BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_BUCKET="test-bucket" 40 | export BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_SOURCE="old-path/" 41 | export BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_DESTINATION="https://example.com/new-path/" 42 | export BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_REGION="us-east-1" 43 | 44 | if [ "${BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_BUCKET}" = "test-bucket" ] && \ 45 | [ "${BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_SOURCE}" = "old-path/" ] && \ 46 | [ "${BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_DESTINATION}" = "https://example.com/new-path/" ]; then 47 | pass "Environment variables set correctly for single redirect" 48 | else 49 | fail "Environment variables not set correctly" 50 | fi 51 | } 52 | 53 | # Test 2: Multiple redirects configuration 54 | test_multiple_redirects() { 55 | print_test "Multiple redirects configuration" 56 | 57 | export BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_BUCKET="test-bucket" 58 | export BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_REDIRECTS_0_SOURCE="path1/" 59 | export BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_REDIRECTS_0_DESTINATION="https://example.com/new1/" 60 | export BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_REDIRECTS_1_SOURCE="path2/" 61 | export BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_REDIRECTS_1_DESTINATION="https://example.com/new2/" 62 | 63 | redirect_count=0 64 | for var in $(env | grep "^BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_REDIRECTS_" | grep "_SOURCE=" | cut -d= -f1); do 65 | redirect_count=$((redirect_count + 1)) 66 | done 67 | 68 | if [ ${redirect_count} -eq 2 ]; then 69 | pass "Found 2 redirects in configuration" 70 | else 71 | fail "Expected 2 redirects, found ${redirect_count}" 72 | fi 73 | } 74 | 75 | # Test 3: Source path normalization 76 | test_source_normalization() { 77 | print_test "Source path normalization" 78 | 79 | # Test removing s3:// prefix 80 | source="s3://test-bucket/old-path/" 81 | bucket="test-bucket" 82 | source="${source#s3://}" 83 | source="${source#"${bucket}"/}" 84 | 85 | if [ "${source}" = "old-path/" ]; then 86 | pass "Correctly normalized s3:// prefix" 87 | else 88 | fail "Failed to normalize s3:// prefix, got: ${source}" 89 | fi 90 | 91 | # Test removing bucket name 92 | source="test-bucket/another-path/" 93 | source="${source#"${bucket}"/}" 94 | 95 | if [ "${source}" = "another-path/" ]; then 96 | pass "Correctly normalized bucket name prefix" 97 | else 98 | fail "Failed to normalize bucket name, got: ${source}" 99 | fi 100 | } 101 | 102 | # Test 4: AWS CLI command construction 103 | test_aws_command_construction() { 104 | print_test "AWS CLI command construction" 105 | 106 | REGION="us-east-1" 107 | AWS_OPTS="--region ${REGION}" 108 | 109 | if [ "${AWS_OPTS}" = "--region us-east-1" ]; then 110 | pass "AWS options constructed correctly" 111 | else 112 | fail "AWS options incorrect: ${AWS_OPTS}" 113 | fi 114 | } 115 | 116 | # Test 5: Required parameters validation 117 | test_required_parameters() { 118 | print_test "Required parameters validation" 119 | 120 | BUCKET="" 121 | if [ -z "${BUCKET}" ]; then 122 | pass "Correctly detects missing bucket parameter" 123 | else 124 | fail "Should detect missing bucket parameter" 125 | fi 126 | 127 | BUCKET="test-bucket" 128 | if [ -n "${BUCKET}" ]; then 129 | pass "Correctly validates bucket parameter is present" 130 | else 131 | fail "Should validate bucket parameter" 132 | fi 133 | } 134 | 135 | # Test 6: Region default value 136 | test_region_default() { 137 | print_test "Region default value" 138 | 139 | unset BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_REGION 2>/dev/null || true 140 | REGION="${BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_REGION:-us-east-1}" 141 | 142 | if [ "${REGION}" = "us-east-1" ]; then 143 | pass "Correctly uses default region" 144 | else 145 | fail "Default region should be us-east-1, got: ${REGION}" 146 | fi 147 | 148 | export BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_REGION="ap-southeast-2" 149 | REGION="${BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_REGION:-us-east-1}" 150 | 151 | if [ "${REGION}" = "ap-southeast-2" ]; then 152 | pass "Correctly uses custom region" 153 | else 154 | fail "Custom region should be ap-southeast-2, got: ${REGION}" 155 | fi 156 | } 157 | 158 | # Test 7: Empty redirects array handling 159 | test_empty_redirects() { 160 | print_test "Empty redirects array handling" 161 | 162 | cleanup_env 163 | export BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_BUCKET="test-bucket" 164 | 165 | redirect_count=0 166 | for var in $(env | grep "^BUILDKITE_PLUGIN_AWS_S3_WEBSITE_REDIRECT_REDIRECTS_" 2>/dev/null | grep "_SOURCE=" | cut -d= -f1); do 167 | redirect_count=$((redirect_count + 1)) 168 | done 169 | 170 | if [ ${redirect_count} -eq 0 ]; then 171 | pass "Correctly handles empty redirects array" 172 | else 173 | fail "Should find no redirects, found ${redirect_count}" 174 | fi 175 | } 176 | 177 | # Run all tests 178 | echo "================================================" 179 | echo "Running s3-website-redirect Plugin Tests" 180 | echo "================================================" 181 | 182 | test_single_redirect 183 | test_multiple_redirects 184 | test_source_normalization 185 | test_aws_command_construction 186 | test_required_parameters 187 | test_region_default 188 | test_empty_redirects 189 | 190 | # Print summary 191 | echo "" 192 | echo "================================================" 193 | echo "Test Summary" 194 | echo "================================================" 195 | echo -e "${GREEN}Passed: ${TESTS_PASSED}${NC}" 196 | echo -e "${RED}Failed: ${TESTS_FAILED}${NC}" 197 | echo "================================================" 198 | 199 | if [ ${TESTS_FAILED} -gt 0 ]; then 200 | exit 1 201 | fi 202 | 203 | exit 0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS S3 Website Redirect Buildkite Plugin 2 | 3 | A Buildkite plugin to add website redirects via Amazon S3 by setting the `x-amz-website-redirect-location` metadata on S3 objects. 4 | 5 | This plugin uses the AWS CLI to create empty S3 objects with redirect metadata, which allows S3 static websites to redirect old URLs to new locations. 6 | 7 | ## How It Works 8 | 9 | The plugin executes the following AWS CLI command for each redirect: 10 | 11 | ```bash 12 | aws s3 cp --website-redirect "https://example.com/my-new-docs/" - "s3://example.com/my-old-docs/" <<< "" 13 | ``` 14 | 15 | This creates an empty object in S3 with the `x-amz-website-redirect-location` metadata set to the destination URL. When a user accesses the old URL on an S3 static website, S3 will return an HTTP 301 redirect to the new location. 16 | 17 | ### Important: Path Redirect Limitation 18 | 19 | **The redirect destination is static and does NOT automatically preserve or append the original path.** 20 | 21 | For example, if you create a redirect from `my-old-docs/` to `https://example.com/my-new-docs/`: 22 | 23 | - ✅ Accessing `https://example.com/my-old-docs/` redirects to `https://example.com/my-new-docs/` 24 | - ❌ Accessing `https://example.com/my-old-docs/page.html` **also** redirects to `https://example.com/my-new-docs/` (NOT to `https://example.com/my-new-docs/page.html`) 25 | 26 | **Best practice**: Redirect specific HTML files to specific HTML files, not directory paths. 27 | 28 | **Workarounds for Path Preservation:** 29 | 30 | 1. **Create individual redirects for each file/page** (what this plugin does - list each redirect explicitly) 31 | 2. **Use S3 website routing rules** in your bucket configuration with `ReplaceKeyPrefixWith` for automatic path substitution 32 | 3. **Use CloudFront Functions or Lambda@Edge** for dynamic redirect logic 33 | 34 | This plugin is best suited for: 35 | - Redirecting specific HTML files or pages to their new locations 36 | - Consolidating multiple old pages to a single landing page 37 | - Setting up redirects for moved or renamed documentation pages 38 | 39 | ## Quick Start 40 | 41 | ### Single Redirect 42 | 43 | ```yaml 44 | steps: 45 | - label: ":books: Publish Docs" 46 | command: "yarn docs build" 47 | plugins: 48 | - envato/aws-s3-sync#v0.5.0: 49 | source: docs/.vitepress/dist/ 50 | destination: s3://example.com/my-project/ 51 | - envato/aws-s3-website-redirect#v0.2.0: 52 | bucket: example.com 53 | source: my-project/old-api-docs.html 54 | destination: https://example.com/my-project/api-reference.html 55 | ``` 56 | 57 | ### Multiple Redirects 58 | 59 | ```yaml 60 | steps: 61 | - label: ":books: Publish Docs" 62 | command: "yarn docs build" 63 | plugins: 64 | - envato/aws-s3-sync#v0.5.0: 65 | source: docs/.vitepress/dist/ 66 | destination: s3://example.com/my-project/ 67 | - envato/aws-s3-website-redirect#v0.2.0: 68 | bucket: example.com 69 | redirects: 70 | - source: my-project/quickstart.html 71 | destination: https://example.com/my-project/getting-started.html 72 | - source: my-project/old-tutorial.html 73 | destination: https://example.com/my-project/tutorial.html 74 | - source: my-project/deprecated-api.html 75 | destination: https://example.com/my-project/api-reference.html 76 | ``` 77 | 78 | ### With Custom Region 79 | 80 | ```yaml 81 | steps: 82 | - label: ":books: Publish Docs" 83 | plugins: 84 | - envato/aws-s3-website-redirect#v0.2.0: 85 | bucket: my-docs-bucket 86 | region: ap-southeast-2 87 | redirects: 88 | - source: my-project/installation.html 89 | destination: https://example.com/my-project/setup.html 90 | ``` 91 | 92 | ## Configuration 93 | 94 | ### Required 95 | 96 | #### `bucket` (string) 97 | 98 | The name of the S3 bucket where the redirects will be created. 99 | 100 | **Example:** `example.com` 101 | 102 | ### Optional 103 | 104 | #### `region` (string) 105 | 106 | The AWS region where the S3 bucket is located. 107 | 108 | **Default:** `us-east-1` 109 | 110 | ### Single Redirect Mode 111 | 112 | #### `source` (string) 113 | 114 | The S3 path to redirect from (relative to the bucket root). 115 | 116 | **Example:** `my-old-docs/` 117 | 118 | #### `destination` (string) 119 | 120 | The full URL to redirect to. 121 | 122 | **Example:** `https://example.com/my-new-docs/` 123 | 124 | ### Multiple Redirects Mode 125 | 126 | #### `redirects` (array) 127 | 128 | An array of redirect objects, each containing: 129 | 130 | - `source` (string, required): The S3 path to redirect from 131 | - `destination` (string, required): The full URL to redirect to 132 | 133 | ## Configuration Summary Table 134 | 135 | | Option | Type | Required | Default | Description | 136 | |--------|------|----------|---------|-------------| 137 | | `bucket` | string | Yes | - | S3 bucket name | 138 | | `source` | string | No* | - | Single redirect source path | 139 | | `destination` | string | No* | - | Single redirect destination URL | 140 | | `redirects` | array | No* | - | Array of redirect objects | 141 | | `region` | string | No | `us-east-1` | AWS region | 142 | 143 | *Either use `source`+`destination` for a single redirect, or `redirects` for multiple redirects. 144 | 145 | ## Integration Examples 146 | 147 | ### Example 1: Add Redirect to Existing Publish Docs Step 148 | 149 | Modify your existing "Publish Docs" step in `.buildkite/pipeline.yml`: 150 | 151 | ```yaml 152 | - label: ":books: Publish Docs" 153 | key: publish-docs 154 | command: 155 | - "yarn install" 156 | - "yarn docs build" 157 | if: build.branch == pipeline.default_branch 158 | plugins: 159 | - envato/aws-assume-role#v0.2.0: 160 | role: arn:aws:iam::123456789012:role/docs-role 161 | - docker#v5.3.0: 162 | image: node:18-alpine 163 | environment: 164 | - CI 165 | - envato/aws-s3-sync#v0.5.0: 166 | source: docs/.vitepress/dist/ 167 | destination: s3://example.com/my-project/ 168 | delete: true 169 | # Add redirects after syncing 170 | - envato/aws-s3-website-redirect#v0.2.0: 171 | bucket: example.com 172 | source: my-project/old-getting-started.html 173 | destination: https://example.com/my-project/getting-started.html 174 | - envato/aws-cloudfront-invalidation#v0.1.0: 175 | distribution-id: EXAMPLEID123 176 | paths: 177 | - /my-project/* 178 | timeout_in_minutes: 10 179 | ``` 180 | 181 | ### Example 2: Multiple Redirects After Deployment 182 | 183 | For projects with multiple renamed or moved pages: 184 | 185 | ```yaml 186 | - label: ":books: Publish Docs with Redirects" 187 | command: 188 | - "yarn docs build" 189 | plugins: 190 | - envato/aws-assume-role#v0.2.0: 191 | role: arn:aws:iam::123456789012:role/docs-role 192 | - envato/aws-s3-sync#v0.5.0: 193 | source: docs/.vitepress/dist/ 194 | destination: s3://example.com/my-project/ 195 | - envato/aws-s3-website-redirect#v0.2.0: 196 | bucket: example.com 197 | region: us-east-1 198 | redirects: 199 | # Redirect old CLI reference to new location 200 | - source: my-project/cli-reference.html 201 | destination: https://example.com/my-project/reference/cli.html 202 | # Redirect deprecated API docs 203 | - source: my-project/api-v1.html 204 | destination: https://example.com/my-project/api-v2.html 205 | # Redirect moved tutorial 206 | - source: my-project/setup-tutorial.html 207 | destination: https://example.com/my-project/getting-started.html 208 | ``` 209 | 210 | ### Example 3: Standalone Redirect Step 211 | 212 | Create a separate step to add redirects without redeploying content: 213 | 214 | ```yaml 215 | - label: ":redirect: Update Documentation Redirects" 216 | command: "echo 'Adding redirects for renamed pages'" 217 | if: build.branch == pipeline.default_branch 218 | plugins: 219 | - envato/aws-assume-role#v0.2.0: 220 | role: arn:aws:iam::123456789012:role/docs-role 221 | - envato/aws-s3-website-redirect#v0.2.0: 222 | bucket: example.com 223 | redirects: 224 | - source: my-project/deprecated-feature.html 225 | destination: https://example.com/my-project/index.html 226 | - source: my-project/old-guide.html 227 | destination: https://example.com/my-project/guide.html 228 | ``` 229 | 230 | ### Example 4: Redirecting Individual Pages 231 | 232 | ```yaml 233 | - envato/aws-s3-website-redirect#v0.2.0: 234 | bucket: example.com 235 | source: my-project/old-page.html 236 | destination: https://example.com/my-project/new-page.html 237 | ``` 238 | 239 | ### Example 5: Redirecting to External Documentation 240 | 241 | ```yaml 242 | - envato/aws-s3-website-redirect#v0.2.0: 243 | bucket: example.com 244 | source: my-project/external-integration.html 245 | destination: https://external-docs.example.com/integration-guide.html 246 | ``` 247 | 248 | ## Common Use Cases 249 | 250 | This plugin is particularly useful for documentation sites where you want to: 251 | 252 | - **Redirect renamed or moved documentation pages** - Maintain links when restructuring docs 253 | - **Maintain backward compatibility for bookmarked URLs** - Don't break existing links 254 | - **Consolidate multiple old paths to a single new location** - Merge deprecated sections 255 | - **Set up temporary redirects during content migration** - Gradual migration support 256 | - **Redirect deprecated features to current equivalents** - Keep users on supported pages 257 | - **Redirect entire sections** - Move documentation categories 258 | 259 | ## Plugin Order 260 | 261 | The plugin runs during the `post-command` hook, so it executes **after** your build command completes successfully. This is the ideal order: 262 | 263 | 1. Build your documentation (`yarn docs build`) 264 | 2. Sync to S3 (`aws-s3-sync` plugin) 265 | 3. **Add redirects** (`s3-website-redirect` plugin) ← This plugin 266 | 4. Invalidate CloudFront cache (`aws-cloudfront-invalidation` plugin) 267 | 268 | ## Requirements 269 | 270 | - AWS CLI must be installed and available in the PATH 271 | - Appropriate AWS credentials must be configured (via IAM role or environment variables) 272 | - The S3 bucket must be configured as a static website 273 | - The IAM role/user must have `s3:PutObject` permissions on the bucket 274 | 275 | ### Required IAM Permissions 276 | 277 | Ensure your IAM role has the `s3:PutObject` permission: 278 | 279 | ```json 280 | { 281 | "Effect": "Allow", 282 | "Action": "s3:PutObject", 283 | "Resource": "arn:aws:s3:::example.com/*" 284 | } 285 | ``` 286 | 287 | ## Testing Locally 288 | 289 | You can test the AWS command manually: 290 | 291 | ```bash 292 | # Single redirect 293 | aws s3 cp \ 294 | --website-redirect "https://example.com/my-project/new-path/" \ 295 | - \ 296 | "s3://example.com/my-project/old-path/" \ 297 | <<< "" 298 | 299 | # Verify the redirect was created 300 | aws s3api head-object \ 301 | --bucket example.com \ 302 | --key my-project/old-path/ \ 303 | --query 'WebsiteRedirectLocation' 304 | 305 | # Test the redirect in action 306 | curl -I https://example.com/my-project/old-path/ 307 | ``` 308 | 309 | Look for a `Location:` header in the curl response showing the redirect destination. 310 | 311 | ## Best Practices 312 | 313 | 1. **Be consistent with file extensions**: 314 | - ✅ `source: my-project/old-page.html` 315 | - ✅ `source: my-project/index.html` 316 | - ⚠️ For directory redirects, use trailing slashes: `source: my-project/old-section/` 317 | 318 | 2. **Use absolute URLs** for destinations: 319 | - ✅ `destination: https://example.com/my-project/new/` 320 | - ❌ `destination: /my-project/new/` 321 | 322 | 3. **Add redirects after syncing** to ensure all content is deployed first 323 | 324 | 4. **Document your redirects** in a separate file or comment in the pipeline 325 | 326 | 5. **Test redirects** before removing old content from your repository 327 | 328 | 6. **Redirect files, not directories** - due to the path limitation, redirect specific HTML files to specific destinations 329 | 330 | 7. **Use meaningful redirect destinations** - redirect to the most relevant replacement page 331 | 332 | 8. **Keep redirects indefinitely** - users may have old bookmarks years later 333 | 334 | ## Migration Example 335 | 336 | When restructuring documentation, deploy new structure first, then add redirects: 337 | 338 | ```yaml 339 | # Step 1: Deploy new structure 340 | - label: ":books: Deploy Restructured Docs" 341 | command: "yarn docs build" 342 | plugins: 343 | - envato/aws-s3-sync#v0.5.0: 344 | source: docs/.vitepress/dist/ 345 | destination: s3://example.com/my-project/ 346 | 347 | # Step 2: Add redirects for all old paths 348 | - label: ":redirect: Add Migration Redirects" 349 | command: "echo 'Setting up redirects...'" 350 | plugins: 351 | - envato/aws-s3-website-redirect#v0.2.0: 352 | bucket: example.com 353 | redirects: 354 | - source: my-project/installation.html 355 | destination: https://example.com/my-project/guide/installation.html 356 | - source: my-project/quickstart.html 357 | destination: https://example.com/my-project/guide/quickstart.html 358 | - source: my-project/api-docs.html 359 | destination: https://example.com/my-project/reference/api.html 360 | - source: my-project/cli-commands.html 361 | destination: https://example.com/my-project/reference/cli.html 362 | 363 | # Step 3: Invalidate cache 364 | - label: ":cloudfront: Invalidate Cache" 365 | command: "echo 'Invalidating CloudFront...'" 366 | plugins: 367 | - envato/aws-cloudfront-invalidation#v0.1.0: 368 | distribution-id: EXAMPLEID123 369 | paths: 370 | - /my-project/* 371 | ``` 372 | 373 | ## Troubleshooting 374 | 375 | ### Permission Denied Errors 376 | 377 | Ensure your IAM role has the `s3:PutObject` permission (see Requirements section above). 378 | 379 | ### Redirects Not Working 380 | 381 | 1. **Verify your S3 bucket is configured for static website hosting** - Check bucket properties 382 | 2. **Check that the source path matches exactly** - Include/exclude trailing slashes consistently 383 | 3. **Test the redirect directly**: `curl -I https://example.com/my-project/old-path/` 384 | 4. **Look for a `Location:` header** in the response showing the redirect destination 385 | 5. **Verify the object exists in S3**: Use `aws s3api head-object` to check the metadata 386 | 387 | ### CloudFront Caching 388 | 389 | If using CloudFront, you may need to invalidate the redirect path to see changes immediately: 390 | 391 | ```yaml 392 | - envato/aws-cloudfront-invalidation#v0.1.0: 393 | distribution-id: EXAMPLEID123 394 | paths: 395 | - /my-project/old-path/ 396 | - /my-project/new-path/ 397 | ``` 398 | 399 | CloudFront will cache the redirect response, so without invalidation, users may see old behavior until the cache expires. 400 | 401 | ### Redirect Goes to Wrong Destination 402 | 403 | Remember that the destination is static. If you're trying to redirect an entire directory tree while preserving paths, consider using S3 website routing rules instead: 404 | 405 | ```xml 406 | 407 | 408 | 409 | my-old-docs/ 410 | 411 | 412 | my-new-docs/ 413 | 414 | 415 | 416 | ``` 417 | 418 | This approach preserves the path structure automatically. The aws-s3-website-redirect Buildkite plugin may implement this feature in the future. 419 | 420 | ## Additional Resources 421 | 422 | - [AWS S3 Website Redirects Documentation](https://docs.aws.amazon.com/AmazonS3/latest/userguide/how-to-page-redirect.html) 423 | - [AWS S3 Website Routing Rules](https://docs.aws.amazon.com/AmazonS3/latest/userguide/how-to-page-redirect.html#advanced-conditional-redirects) 424 | - [Buildkite Plugin Documentation](https://buildkite.com/docs/plugins) 425 | 426 | ## License 427 | 428 | MIT (see [LICENSE](LICENSE) file) 429 | --------------------------------------------------------------------------------