Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 58 additions & 36 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ on:
type: string
default: ""
description: "Path to a custom WXS file in the caller repo for MSI generation (relative to repo root). If not provided, uses default template."
windows_pre_build_script:
required: false
type: string
default: ""
description: "Optional safe relative Bash script in the caller repo, run by GoReleaser before Windows compilation."
brew:
required: false
type: boolean
Expand Down Expand Up @@ -144,47 +149,12 @@ jobs:
exit 1
fi

- name: Validate dockerfile_template has safe path
if: inputs.dockerfile_template != ''
env:
DOCKERFILE_TEMPLATE: ${{ inputs.dockerfile_template }}
run: |
if [[ "$DOCKERFILE_TEMPLATE" == /* ]] || [[ "$DOCKERFILE_TEMPLATE" == *".."* ]] || [[ ! "$DOCKERFILE_TEMPLATE" =~ ^[A-Za-z0-9._/-]+$ ]]; then
echo "::error::dockerfile_template must be a relative path with safe characters and without '..' traversal. Got: $DOCKERFILE_TEMPLATE"
exit 1
fi

- name: Validate docker_extra_files requires dockerfile_template
if: inputs.docker_extra_files != '' && inputs.dockerfile_template == ''
run: |
echo "::error::docker_extra_files can only be used when dockerfile_template is set"
exit 1

- name: Validate docker_extra_files have safe paths
if: inputs.docker_extra_files != ''
env:
DOCKER_EXTRA_FILES: ${{ inputs.docker_extra_files }}
run: |
IFS=',' read -ra FILES <<< "$DOCKER_EXTRA_FILES"
for file in "${FILES[@]}"; do
file="${file#"${file%%[![:space:]]*}"}"
file="${file%"${file##*[![:space:]]}"}"
if [[ -z "$file" ]] || [[ "$file" == /* ]] || [[ "$file" == *".."* ]] || [[ ! "$file" =~ ^[A-Za-z0-9._/-]+$ ]]; then
echo "::error::docker_extra_files entries must be relative paths with safe characters and without '..' traversal. Got: $file"
exit 1
fi
done

- name: Validate msi_wxs_path has no path traversal
if: inputs.msi_wxs_path != ''
env:
WXS_PATH: ${{ inputs.msi_wxs_path }}
run: |
if [[ "$WXS_PATH" == /* ]] || [[ "$WXS_PATH" == *".."* ]] || [[ ! "$WXS_PATH" =~ ^[A-Za-z0-9._/-]+$ ]]; then
echo "::error::msi_wxs_path must be a relative path with safe characters and without '..' traversal. Got: $WXS_PATH"
exit 1
fi

- name: Validate GORELEASER_PRO_KEY when msi enabled
if: inputs.msi == true
env:
Expand Down Expand Up @@ -222,6 +192,55 @@ jobs:
ref: ${{ steps.workflow-version.outputs.sha }}
persist-credentials: false

- name: Validate dockerfile_template has safe path
if: inputs.dockerfile_template != ''
working-directory: _workflows
env:
DOCKERFILE_TEMPLATE: ${{ inputs.dockerfile_template }}
run: |
if ! bash ./scripts/validate-safe-relative-path.sh "$DOCKERFILE_TEMPLATE"; then
echo "::error::dockerfile_template must be a relative path with safe characters and without '..' traversal. Got: $DOCKERFILE_TEMPLATE"
exit 1
fi

- name: Validate docker_extra_files have safe paths
if: inputs.docker_extra_files != ''
working-directory: _workflows
env:
DOCKER_EXTRA_FILES: ${{ inputs.docker_extra_files }}
run: |
IFS=',' read -ra FILES <<< "$DOCKER_EXTRA_FILES"
for file in "${FILES[@]}"; do
file="${file#"${file%%[![:space:]]*}"}"
file="${file%"${file##*[![:space:]]}"}"
if ! bash ./scripts/validate-safe-relative-path.sh "$file"; then
echo "::error::docker_extra_files entries must be relative paths with safe characters and without '..' traversal. Got: $file"
exit 1
fi
done

- name: Validate msi_wxs_path has no path traversal
if: inputs.msi_wxs_path != ''
working-directory: _workflows
env:
WXS_PATH: ${{ inputs.msi_wxs_path }}
run: |
if ! bash ./scripts/validate-safe-relative-path.sh "$WXS_PATH"; then
echo "::error::msi_wxs_path must be a relative path with safe characters and without '..' traversal. Got: $WXS_PATH"
exit 1
fi

- name: Validate windows_pre_build_script has safe path
if: inputs.windows_pre_build_script != ''
working-directory: _workflows
env:
WINDOWS_PRE_BUILD_SCRIPT: ${{ inputs.windows_pre_build_script }}
run: |
if ! bash ./scripts/validate-safe-relative-path.sh "$WINDOWS_PRE_BUILD_SCRIPT"; then
echo "::error::windows_pre_build_script must be a relative path with safe characters and without '..' traversal. Got: $WINDOWS_PRE_BUILD_SCRIPT"
exit 1
fi

- name: Normalize release options
id: release-options
working-directory: _workflows
Expand Down Expand Up @@ -747,11 +766,13 @@ jobs:
WXS_PATH: ${{ steps.wxs.outputs.wxs_path }}
WORKFLOWS_REF: ${{ needs.determine-workflows-ref.outputs.ref }}
RELEASE_TAG: ${{ inputs.tag }}
WINDOWS_PRE_BUILD_SCRIPT: ${{ inputs.windows_pre_build_script }}
run: |
set -euo pipefail
export BUILD_STARTED_ON=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

# Generate GoReleaser config
envsubst < templates/.goreleaser-windows-template.yaml.tmpl | tee "_generated/.goreleaser.windows.yaml"
bash ./scripts/render-goreleaser-windows-config.sh "$WINDOWS_PRE_BUILD_SCRIPT" ../_caller | tee "_generated/.goreleaser.windows.yaml"

# Generate provenance predicate
envsubst < templates/.slsa-provenance-predicate-template.json.tmpl | tee "_generated/predicate.json"
Expand All @@ -766,6 +787,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.RELENG_GITHUB_TOKEN }}
GORELEASER_CURRENT_TAG: ${{ inputs.tag }}
RELEASE_TAG: ${{ inputs.tag }}
GORELEASER_KEY: ${{ secrets.GORELEASER_PRO_KEY }}

- name: Flatten MSI directory structure
Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ test-scripts:
bash scripts/test-derive-iam-role-name.sh
bash scripts/test-normalize-release-options.sh
python3 scripts/test-release-config-templates.py
python3 scripts/test-windows-pre-build-script.py
python3 scripts/test-safe-relative-path.py
bash scripts/test-release-workflow-tag-pin.sh
bash scripts/test-s3-release-uploads.sh
bash scripts/test-validate-release-artifacts.sh
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ The release workflow accepts the following input parameters:
| `docker_extra_files` | No | `""` | Comma-separated list of extra files/dirs to include in Docker build context |
| `msi` | No | `true` | Whether to build MSI Windows installers |
| `msi_wxs_path` | No | `""` | Path to custom WXS template for MSI installer (uses default if not set) |
| `windows_pre_build_script` | No | `""` | Optional safe repo-relative Bash script, run in the caller checkout by GoReleaser before Windows compilation with `RELEASE_TAG` set to the requested tag. |
| `brew` | No | `true` | Whether to publish a Homebrew formula to the selected public `conductorone` tap |

2. Ensure your repository has the following secrets configured:
Expand Down Expand Up @@ -250,6 +251,29 @@ To disable MSI builds entirely (e.g., for connectors that don't need Windows ins

When `msi: false`, the `GORELEASER_PRO_KEY` secret is not required.

### Windows Pre-build Script

Connectors that generate Windows-specific resources from the release tag can opt in:

```yaml
with:
tag: ${{ github.ref_name }}
windows_pre_build_script: scripts/release-windows-resources.sh
```

The script must exist as a file in the tagged caller repository. Paths must use
only letters, digits, `.`, `_`, `/`, and `-`, and must not be absolute or contain
`..` or whitespace. GoReleaser runs `bash ./<path>` in `_caller` as a
`before.hooks` command, after its normal clean-tree/tag validation and before
Windows compilation. The hook receives the validated workflow tag as
`RELEASE_TAG`; a failing hook fails the Windows release.

This is Windows-only and opt-in: an empty input leaves the rendered Windows
config unchanged and does not run any generation. macOS, Linux, and Docker
builds do not run this hook. Keep connector-specific generation in the caller's
script; do not skip release validation, use snapshots, retag, or commit generated
resources during release.

## Verify Workflow

Runs linting, tests, and optional regression verification. See [detailed documentation](docs/verify-workflow.md) for jobs, regression testing, and all options.
Expand Down
37 changes: 36 additions & 1 deletion docs/release-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,21 @@ Validates workflow inputs before proceeding:
`^[a-z][a-z0-9-]{0,99}$`
- Ensures `dockerfile_template` is only used when `lambda: false`
- Ensures `docker_extra_files` is only used when `dockerfile_template` is set
- Ensures `msi_wxs_path` has no path traversal (`..` or absolute paths)
- Ensures `GORELEASER_PRO_KEY` is provided when `msi: true`

### determine-workflows-ref

Resolves the exact SHA of the shared workflow, then uses the pinned workflow source to normalize `go_main_package` and `brew_tap`. An omitted main package becomes `./cmd/<repository-name>`; a root package uses `./`. The workflow rejects non-relative package paths and tap values containing a path separator. The pinned reference is embedded in all provenance attestations, ensuring verifiability.

Before option normalization, the pinned checkout validates `dockerfile_template`,
`msi_wxs_path`, `windows_pre_build_script`, and trimmed `docker_extra_files`
entries through `scripts/validate-safe-relative-path.sh`. The helper has the
single safe-path predicate; callers retain their existing diagnostics and
empty-input guards.
These guarded checks run after the existing pinned checkout rather than adding
an unpinned fetch or permissions to `validate-inputs`; all builds already depend
on `determine-workflows-ref`. No job dependencies or permissions change.

### release-preflight

Runs opted-in source integrity checks against the exact tagged caller source before any build job:
Expand Down Expand Up @@ -69,13 +77,26 @@ Builds Windows zip and MSI installer:
- MSI built using WiX Toolset with GoReleaser Pro
- Deterministic UpgradeCode via UUID v5 from repository name
- Supports custom WXS templates via `msi_wxs_path` input
- Supports an opt-in caller Bash script via `windows_pre_build_script`
- Generates SBOMs and SLSA v1 provenance attestations
- Uploads all artifacts to S3 with no-overwrite writes

**Outputs:** `*.zip`, `*.msi`, `*.provenance.sigstore.json`, `*.sbom.sigstore.json`

**Custom WXS:** For connectors requiring custom MSI behavior (Windows Service, registry keys, etc.), provide a custom WXS template via `msi_wxs_path`. If not provided, uses the default CLI installer template.

**Windows pre-build script:** `windows_pre_build_script` defaults to empty,
preserving the original rendered GoReleaser config without a hook. When set,
Windows config generation verifies that the safe repo-relative path exists as a
file in `_caller`. The config adds a `before.hooks` command, `bash ./<path>`,
which GoReleaser runs in that same tagged caller checkout after its normal
clean-tree/tag validation and before compilation. Only the Windows GoReleaser
action forwards `RELEASE_TAG` from the validated `tag` input to the hook. Hook
failures fail the release; generated resources remain local to the Windows
build workspace. The workflow does not run this hook on macOS, Linux, Docker,
or callers with the default empty input, and does not skip validation, build
snapshots, retag, or commit generated resources.

### goreleaser-docker (Linux)

Builds and publishes container images:
Expand Down Expand Up @@ -293,6 +314,20 @@ Use test connectors for validation:
./scripts/validate-release-artifacts.sh ConductorOne/baton-github-test v0.1.xxx
```

### Local Windows Hook Checks

`make test-scripts` includes `python3 scripts/test-windows-pre-build-script.py`.
It uses the real workflow Bash steps and `envsubst`, with `yq` for YAML queries:
empty input must match the frozen pre-hook config fixture byte-for-byte; opted-in
scripts must render the expected command; unsafe and missing paths must fail.
The fixture hook also checks the caller working directory, release-tag
forwarding, and command failure propagation without compiling or publishing.
These are local fixture checks, not a deployed Windows release proof.

`python3 scripts/test-safe-relative-path.py` additionally compares the shared
predicate and every workflow/render call site's exit status and diagnostic bytes
against the previous Bash rules, including extra-file trimming and empty guards.

### Validation Script

The `scripts/validate-release-artifacts.sh` script validates:
Expand Down
27 changes: 27 additions & 0 deletions scripts/render-goreleaser-windows-config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail

if [ "$#" -ne 2 ]; then
echo "usage: $0 <windows-pre-build-script> <caller-root>" >&2
exit 2
fi

windows_pre_build_script="$1"
caller_root="$2"
WINDOWS_BEFORE_HOOKS_BLOCK=""
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

if [ -n "$windows_pre_build_script" ]; then
if ! bash "$script_dir/validate-safe-relative-path.sh" "$windows_pre_build_script"; then
echo "::error::windows_pre_build_script must be a relative path with safe characters and without '..' traversal. Got: $windows_pre_build_script" >&2
exit 1
fi
if [ ! -f "$caller_root/$windows_pre_build_script" ]; then
echo "::error::Windows pre-build script not found: $windows_pre_build_script" >&2
exit 1
fi
printf -v WINDOWS_BEFORE_HOOKS_BLOCK 'before:\n hooks:\n - "bash ./%s"\n' "$windows_pre_build_script"
fi
export WINDOWS_BEFORE_HOOKS_BLOCK

envsubst < "$script_dir/../templates/.goreleaser-windows-template.yaml.tmpl"
1 change: 1 addition & 0 deletions scripts/test-release-config-templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def verify_case(go_main_package: str, brew_tap: str) -> None:
"DOCKERFILE_LAMBDA_PATH": "Dockerfile.lambda",
"PUBLIC_ECR_PUBLISH_TAG": "candidate",
"EXTRA_FILES_BLOCK": "",
"WINDOWS_BEFORE_HOOKS_BLOCK": "",
}
rendered = {name: render(path, values) for name, path in TEMPLATES.items()}

Expand Down
Loading
Loading