diff --git a/.env.sample b/.env.sample index 7b57e7a..6c9a76c 100644 --- a/.env.sample +++ b/.env.sample @@ -6,3 +6,4 @@ QUAY_USERNAME=XXXX QUAY_TOKEN=XXXX DOCKERHUB_USERNAME=XXXX DOCKERHUB_TOKEN=XXXX +SNYK_TOKEN=XXXX diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index 639afdd..1f3119f 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -21,6 +21,9 @@ jobs: run: | pip install pre-commit make pre-commit + - name: Run tests + run: | + make test gitleaks: name: gitleaks diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66c04f1..426a017 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -94,7 +94,7 @@ repos: # run semgrep on all known filetypes - repo: https://github.com/returntocorp/semgrep - rev: 'v1.84.1' + rev: 'v1.89.0' hooks: - id: semgrep # See https://semgrep.dev/explore to select a ruleset and copy its URL diff --git a/Dockerfile b/Dockerfile index e839363..ce74daf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,15 +8,17 @@ RUN go mod download COPY . ./ -FROM init as vet +FROM init AS vet RUN go vet ./... # run tests -FROM init as test -RUN go test -coverprofile c.out -v ./... +FROM init AS test +RUN go test -coverprofile c.out -v ./... && \ + echo "Statements missing coverage" && \ + grep -v -e " 1$" c.out # build binary -FROM init as build +FROM init AS build ARG LDFLAGS RUN CGO_ENABLED=0 go build -ldflags="${LDFLAGS}" diff --git a/Dockerfile.distroless b/Dockerfile.distroless index 34345cc..24c0961 100644 --- a/Dockerfile.distroless +++ b/Dockerfile.distroless @@ -8,15 +8,15 @@ RUN go mod download COPY . ./ -FROM init as vet +FROM init AS vet RUN go vet ./... # run tests -FROM init as test +FROM init AS test RUN go test -coverprofile c.out -v ./... # build binary -FROM init as build +FROM init AS build ARG LDFLAGS RUN CGO_ENABLED=0 go build -ldflags="${LDFLAGS}" diff --git a/Makefile b/Makefile index 70dbb1a..a0e1764 100644 --- a/Makefile +++ b/Makefile @@ -44,9 +44,7 @@ vet: ## Run `go vet` in Docker docker build --target vet -f $(CURDIR)/Dockerfile -t toozej/golang-starter:latest . test: ## Run `go test` in Docker - docker build --target test -f $(CURDIR)/Dockerfile -t toozej/golang-starter:latest . - @echo -e "\nStatements missing coverage" - @grep -v -e " 1$$" c.out + docker build --progress=plain --target test -f $(CURDIR)/Dockerfile -t toozej/golang-starter:latest . build: ## Build Docker image, including running tests docker build -f $(CURDIR)/Dockerfile -t toozej/golang-starter:latest . @@ -123,6 +121,9 @@ install: local-build local-verify ## Install compiled binary to local machine sudo cp $(CURDIR)/out/golang-starter /usr/local/bin/golang-starter sudo chmod 0755 /usr/local/bin/golang-starter +assert-secrets-gh: ## Assert secrets from .env to GitHub Actions Secrets + $(CURDIR)/scripts/upload_secrets_to_github.sh golang-starter + docker-login: ## Login to Docker registries used to publish images to if test -e $(CURDIR)/.env; then \ export `cat $(CURDIR)/.env | xargs`; \ diff --git a/go.mod b/go.mod index 70afa53..8f5daf2 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/muesli/roff v0.1.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.1 - go.uber.org/automaxprocs v1.5.3 + go.uber.org/automaxprocs v1.6.0 ) require ( @@ -20,7 +20,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/muesli/mango v0.2.0 // indirect github.com/muesli/mango-pflag v0.1.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/sagikazarmark/locafero v0.6.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -31,9 +31,9 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/text v0.17.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a017832..19d9af3 100644 --- a/go.sum +++ b/go.sum @@ -164,6 +164,8 @@ github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeB github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -257,6 +259,8 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -297,6 +301,8 @@ golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0J golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -430,6 +436,8 @@ golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -453,6 +461,8 @@ golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/scripts/completions.sh b/scripts/completions.sh index 8a42d4e..c513c78 100755 --- a/scripts/completions.sh +++ b/scripts/completions.sh @@ -3,5 +3,5 @@ set -e rm -rf completions mkdir completions for sh in bash zsh fish; do - go run ./cmd/golang-starter/ completion "$sh" >"completions/golang-starter.$sh" + go run main.go completion "$sh" >"completions/golang-starter.$sh" done diff --git a/scripts/upload_secrets_to_github.sh b/scripts/upload_secrets_to_github.sh new file mode 100755 index 0000000..3a1f8d4 --- /dev/null +++ b/scripts/upload_secrets_to_github.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# Helper function for error handling +function handle_error { + echo "Error: $1" + exit 1 +} + +# Validate that .env exists +if [[ ! -f .env ]]; then + handle_error ".env file not found. Ensure it exists before running this script." +fi + +# Read GitHub username and token from the environment +GITHUB_USERNAME="${GITHUB_USERNAME:-}" +GITHUB_TOKEN="${GITHUB_TOKEN:-}" + +if [[ -z "$GITHUB_USERNAME" || -z "$GITHUB_TOKEN" ]]; then + handle_error "GITHUB_USERNAME or GITHUB_TOKEN is not set in the environment. Please set them in .env." +fi + +# Helper function to upload secrets to GitHub Actions +upload_secrets_to_github() { + echo "Pushing .env entries to GitHub Actions secrets for repo: $GITHUB_USERNAME/$REPO_NAME..." + + while IFS='=' read -r key value; do + if [[ "$key" != "" ]]; then + response=$(curl -s -X PUT \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"encrypted_value\":\"$value\",\"key_id\":\"$key\"}" \ + "https://api.github.com/repos/$GITHUB_USERNAME/$REPO_NAME/actions/secrets/$key") + + if [[ "$response" == *"errors"* ]]; then + handle_error "Failed to set secret $key in GitHub Actions. Response: $response" + fi + fi + done < .env + echo "Secrets successfully uploaded to GitHub Actions." +} + +# Main script logic +REPO_NAME="$1" + +if [[ -z "$REPO_NAME" ]]; then + handle_error "Usage: $0 " +fi + +# Execute the function to upload secrets +upload_secrets_to_github diff --git a/scripts/use_starter.sh b/scripts/use_starter.sh index ba0b35a..0fb4092 100755 --- a/scripts/use_starter.sh +++ b/scripts/use_starter.sh @@ -1,6 +1,137 @@ #!/usr/bin/env bash set -Eeuo pipefail +# --- Functions --- + +# Helper function for error handling +function handle_error { + echo "Error: $1" + exit 1 +} + +# Helper function to fetch credentials from 1Password +fetch_credentials() { + echo "Fetching credentials from 1Password..." + + GITHUB_GHCR_TOKEN=$(op item get "github.com" --field ghcr_token) || handle_error "Failed to fetch GitHub GHCR token." + DOCKERHUB_USERNAME=$(op item get "docker.com" --field username) || handle_error "Failed to fetch DockerHub username." + DOCKERHUB_TOKEN=$(op item get "docker.com" --field token) || handle_error "Failed to fetch DockerHub token." + QUAY_USERNAME=$(op item get "Quay.io" --field username) || handle_error "Failed to fetch Quay username." + QUAY_TOKEN=$(op item get "Quay.io" --field password) || handle_error "Failed to fetch Quay password." + SNYK_TOKEN=$(op item get "Snyk" --field password) || handle_error "Failed to fetch Snyk token." + + # Write environment variables to .env file + cat <> .env +GITHUB_USERNAME=${GITHUB_USERNAME} +GITHUB_GHCR_TOKEN=${GITHUB_GHCR_TOKEN} +DOCKERHUB_USERNAME=${DOCKERHUB_USERNAME} +DOCKERHUB_TOKEN=${DOCKERHUB_TOKEN} +QUAY_USERNAME=${QUAY_USERNAME} +QUAY_TOKEN=${QUAY_TOKEN} +SNYK_TOKEN=${SNYK_TOKEN} +EOF + + echo ".env file created successfully." +} + +# Helper function to generate a GitHub fine-grained token +# TODO validate and re-enable function once GitHub allows you to create fine-grained +# tokens via API calls +# https://developer.github.com/changes/2020-02-14-deprecating-oauth-auth-endpoint/ +# https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens +# +# generate_github_token() { +# echo "Creating GitHub fine-grained token for $NEW_PROJECT_NAME..." +# +# GITHUB_API_URL="https://api.github.com/user/repos" +# GITHUB_TOKEN_NAME="${NEW_PROJECT_NAME}_token" +# +# # Define token permissions +# TOKEN_PERMISSIONS=$(jq -n --argjson permissions '{ +# "actions": "write", +# "code_scanning_alerts": "write", +# "commit_statuses": "write", +# "contents": "write", +# "dependabot_alerts": "write", +# "dependabot_secrets": "write", +# "deployments": "write", +# "environments": "write", +# "issues": "write", +# "pages": "write", +# "pull_requests": "write", +# "secret_scanning_alerts": "write", +# "secrets": "write", +# "webhooks": "write", +# "workflows": "write" +# }') +# +# # Create token using GitHub API +# GITHUB_FG_TOKEN=$(curl -s -X POST \ +# -H "Authorization: token $GITHUB_TOKEN" \ +# -H "Content-Type: application/json" \ +# -d '{ +# "name": "'$GITHUB_TOKEN_NAME'", +# "permissions": '$TOKEN_PERMISSIONS' +# }' $GITHUB_API_URL) +# +# if [[ -z "$GITHUB_FG_TOKEN" ]]; then +# handle_error "Failed to create GitHub fine-grained token." +# fi +# +# TOKEN=$(echo $GITHUB_FG_TOKEN | jq -r '.token') +# +# # Add the GitHub token to the .env file +# echo "GITHUB_FG_TOKEN=$TOKEN" >> .env || handle_error "Failed to write GitHub token to .env." +# } + +# Helper function to generate cosign key-pair +generate_cosign_keys() { + echo "Generating cosign key-pair..." + COSIGN_PASSPHRASE=$(openssl rand -base64 48 | tr -d "=+/" | cut -c1-32) || handle_error "Failed to generate cosign passphrase." + + # Export passphrase for cosign to use + export COSIGN_PASSWORD=${COSIGN_PASSPHRASE} + + # Generate key-pair + cosign generate-key-pair || handle_error "Cosign key generation failed." + + # Rename the cosign keys + mv cosign.key "${NEW_PROJECT_NAME}.key" || handle_error "Failed to rename cosign key." + mv cosign.pub "${NEW_PROJECT_NAME}.pub" || handle_error "Failed to rename cosign pub key." + + # Add cosign passphrase to .env + echo "COSIGN_PASSWORD=${COSIGN_PASSPHRASE}" >> .env || handle_error "Failed to write cosign passphrase to .env." +} + +# Helper function to store secrets in 1Password +store_in_1password() { + echo "Storing secrets in 1Password..." + + # Check if the item exists; if not, create it + if ! op item get "${NEW_PROJECT_NAME}" &>/dev/null; then + # Create the 1Password item with the project name + op item create --category login --title "${NEW_PROJECT_NAME}" \ + --url "https://github.com/${GITHUB_USERNAME}/${NEW_PROJECT_NAME}" \ + --tags "Projects/${NEW_PROJECT_NAME}" || handle_error "Failed to create 1Password item." + fi + + # Update the 1Password item with generated secrets + op item edit "${NEW_PROJECT_NAME}" \ + --field "Cosign Passphrase"="${COSIGN_PASSPHRASE}" \ + --field "GH PAT"="${GITHUB_TOKEN}" \ + --file "${NEW_PROJECT_NAME}.key" \ + --file "${NEW_PROJECT_NAME}.pub" || handle_error "Failed to update 1Password item with secrets." + + echo "Secrets successfully stored in 1Password." +} + +# --- Main Script --- + +# Validate script arguments +if [[ $# -lt 1 ]]; then + handle_error "Usage: $0 [github_username]" +fi + OLD_PROJECT_NAME="golang-starter" NEW_PROJECT_NAME="${1}" GITHUB_USERNAME="${2:-toozej}" @@ -8,21 +139,51 @@ GITHUB_USERNAME="${2:-toozej}" GIT_REPO_ROOT=$(git rev-parse --show-toplevel) cd "${GIT_REPO_ROOT}" -# truncate existing CREDITS.md file and replace its contents with link to template repo's CREDITS.md file +# Register new project's GitHub fine-grained token +read -r -s -p "Enter ${NEW_PROJECT_NAME}'s GH fine-grained PAT from webpage: " GITHUB_TOKEN +cat < .env +GITHUB_TOKEN=${GITHUB_TOKEN} +EOF + +# Update project files +echo "Updating project from ${OLD_PROJECT_NAME} to ${NEW_PROJECT_NAME}..." + +# Truncate existing CREDITS.md file and replace its contents with link to template repo's CREDITS.md file echo -e "# Credits and Acknowledgements\n\n- https://raw.githubusercontent.com/toozej/golang-starter/main/CREDITS.md" > CREDITS.md -# remove golang-starter.pub key -rm -f ./golang-starter.pub +# Remove old public key if it exists +rm -f "./${OLD_PROJECT_NAME}.pub" || handle_error "Failed to remove ${OLD_PROJECT_NAME}.pub" -# update go module name +# Update go module name # shellcheck disable=SC2086 -go mod edit -module=github.com/${GITHUB_USERNAME}/${NEW_PROJECT_NAME} +go mod edit -module=github.com/${GITHUB_USERNAME}/${NEW_PROJECT_NAME} || handle_error "Failed to update go module name." + +# Move directories to match new project name +mv "cmd/${OLD_PROJECT_NAME}" "cmd/${NEW_PROJECT_NAME}" || handle_error "Failed to move project directories." + +# Replace old project name with the new project name across files +grep --exclude-dir=.git -rl "${OLD_PROJECT_NAME}" . | xargs sed -i "" -e "s/${OLD_PROJECT_NAME}/${NEW_PROJECT_NAME}/g" || handle_error "Failed to rename instances of ${OLD_PROJECT_NAME} to ${NEW_PROJECT_NAME}." + +# Show git diff to allow verification of changes +git diff || handle_error "Failed to show git diff." + +# Initialize project environment +echo "Initializing project environment..." + +# Fetch credentials from 1Password +fetch_credentials + +# Generate GitHub fine-grained token +# TODO re-enable generate_github_token function once verified working +# generate_github_token + +# Generate cosign key-pair +generate_cosign_keys -# move directories -mv "cmd/${OLD_PROJECT_NAME}" "cmd/${NEW_PROJECT_NAME}" +# Store generated secrets in 1Password +store_in_1password -# rename from $OLD_PROJECT_NAME to $NEW_PROJECT_NAME -grep --exclude-dir=.git -rl "${OLD_PROJECT_NAME}" . | xargs sed -i "" -e "s/${OLD_PROJECT_NAME}/${NEW_PROJECT_NAME}/g" +# Call the external secrets upload script +./scripts/upload_secrets_to_github.sh "${NEW_PROJECT_NAME}" -# show diff output so user can verify their changes -git diff +echo "Project initialization complete! You can now verify and commit the changes."