Goal supports configuring multiple package registries for publishing your projects.
| Registry | Language | Default URL | Token Environment |
|---|---|---|---|
| PyPI | Python | https://pypi.org/simple/ | PYPI_TOKEN |
| Test PyPI | Python | https://test.pypi.org/simple/ | TEST_PYPI_TOKEN |
| npm | Node.js | https://registry.npmjs.org/ | NPM_TOKEN |
| GitHub Packages | Node.js | https://npm.pkg.github.com/ | GITHUB_TOKEN |
| crates.io | Rust | https://crates.io/ | CARGO_REGISTRY_TOKEN |
| Gemfury | Ruby | https://rubygems.org/ | GEMFURY_TOKEN |
| Private Registry | Any | Custom URL | Custom |
# goal.yaml
registries:
pypi:
url: "https://pypi.org/simple/"
token_env: "PYPI_TOKEN"
registries:
pypi:
url: "https://pypi.org/simple/"
token_env: "PYPI_TOKEN"
testpypi:
url: "https://test.pypi.org/simple/"
token_env: "TEST_PYPI_TOKEN"
private:
url: "https://pypi.mycompany.com/simple/"
token_env: "PRIVATE_PYPI_TOKEN"
registries:
pypi:
url: "https://pypi.org/simple/"
token_env: "PYPI_TOKEN"
strategies:
python:
publish: "twine upload dist/*"
registries:
testpypi:
url: "https://test.pypi.org/simple/"
token_env: "TEST_PYPI_TOKEN"
strategies:
python:
publish: "twine upload --repository testpypi dist/*"
registries:
private:
url: "https://pypi.company.com/simple/"
token_env: "COMPANY_PYPI_TOKEN"
username_env: "COMPANY_PYPI_USER"
strategies:
python:
publish: |
twine upload \
--repository-url https://pypi.company.com/legacy/ \
--username $COMPANY_PYPI_USER \
--password $COMPANY_PYPI_TOKEN \
dist/*
strategies:
python:
publish: |
# Publish to Test PyPI first
twine upload --repository testpypi dist/*
# Wait for propagation
sleep 30
# Then publish to production
twine upload dist/*
registries:
npm:
url: "https://registry.npmjs.org/"
token_env: "NPM_TOKEN"
strategies:
nodejs:
publish: "npm publish"
registries:
github:
url: "https://npm.pkg.github.com/"
token_env: "GITHUB_TOKEN"
strategies:
nodejs:
publish: "npm publish --registry https://npm.pkg.github.com/"
registries:
private:
url: "https://npm.company.com/"
token_env: "COMPANY_NPM_TOKEN"
strategies:
nodejs:
publish: "npm publish --registry https://npm.company.com/"
strategies:
nodejs:
publish: "npm publish --access public"
# For private scoped packages:
# publish: "npm publish --access private"
registries:
cargo:
url: "https://crates.io/"
token_env: "CARGO_REGISTRY_TOKEN"
strategies:
rust:
publish: "cargo publish"
registries:
private_cargo:
url: "https://crates.company.com/"
token_env: "COMPANY_CARGO_TOKEN"
strategies:
rust:
publish: "cargo publish --registry private"
registries:
rubygems:
url: "https://rubygems.org/"
token_env: "RUBYGEMS_API_KEY"
strategies:
ruby:
publish: "gem push *.gem"
registries:
gemfury:
url: "https://push.fury.io/"
token_env: "GEMFURY_TOKEN"
strategies:
ruby:
publish: "gem push *.gem --host https://push.fury.io/username/"
Most registries use API tokens:
# Set environment variables
export PYPI_TOKEN="pypi-xxxxxx"
export NPM_TOKEN="npm_xxxxxx"
export GITHUB_TOKEN="ghp_xxxxxx"
export CARGO_REGISTRY_TOKEN="xxxxxx"
env:
PYPI_TOKEN: $
NPM_TOKEN: $
variables:
PYPI_TOKEN: $PYPI_TOKEN
NPM_TOKEN: $NPM_TOKEN
environment {
PYPI_TOKEN = credentials('pypi-token')
NPM_TOKEN = credentials('npm-token')
}
Some registries require username/password:
registries:
private:
url: "https://registry.company.com/"
username_env: "REGISTRY_USER"
password_env: "REGISTRY_PASS"
strategies:
python:
publish: |
twine upload \
--username $REGISTRY_USER \
--password $REGISTRY_PASS \
dist/*
strategies:
python:
publish: |
if [ "$BRANCH" = "main" ]; then
twine upload dist/*
else
echo "Not publishing from branch $BRANCH"
fi
strategies:
python:
publish: "twine upload --skip-existing dist/*"
nodejs:
publish: "npm publish --dry-run"
rust:
publish: "cargo publish --dry-run"
strategies:
python:
publish: |
twine upload dist/*
echo "Waiting for index update..."
sleep 60
curl -X POST "$NOTIFICATION_WEBHOOK" \
-d "text='Package published to PyPI'"
strategies:
nodejs:
publish: |
# Publish to npm
npm publish
# Publish to GitHub Packages
npm publish --registry https://npm.pkg.github.com/
# Publish to private registry
npm publish --registry https://npm.company.com/
# .goal/development.yaml
registries:
npm:
url: "http://localhost:4873/"
token_env: "DEV_NPM_TOKEN"
# .goal/staging.yaml
registries:
pypi:
url: "https://test.pypi.org/simple/"
token_env: "STAGING_PYPI_TOKEN"
# .goal/production.yaml
registries:
pypi:
url: "https://pypi.org/simple/"
token_env: "PROD_PYPI_TOKEN"
Never hardcode tokens in configuration:
# Bad
registries:
pypi:
token: "pypi-xxxxxx"
# Good
registries:
pypi:
token_env: "PYPI_TOKEN"
Use minimal permission tokens:
# PyPI - scoped to specific package
pypi-token --project my-package
# npm - automation-only token
npm token create --read-only false
Regularly rotate tokens:
hooks:
post_push: |
# Notify to rotate token
curl -X POST "$SECURITY_WEBHOOK" \
-d "text='Remember to rotate $REGISTRY token'"
Log publishing activity:
strategies:
python:
publish: |
echo "Publishing to PyPI at $(date)" >> publish.log
twine upload dist/*
echo "Published successfully at $(date)" >> publish.log
# Test PyPI token
twine check dist/*
# Test npm token
npm whoami
# Test cargo token
cargo login --registry crates.io
# Verify URL format
registries:
pypi:
url: "https://pypi.org/simple/" # Must end with /
# Check token permissions
strategies:
python:
publish: |
twine upload --verbose dist/* # Shows error details
# Use proxy if needed
strategies:
python:
publish: |
https_proxy=$HTTPS_PROXY twine upload dist/*
# goal.yaml
project:
name: "my-package"
type: ["python"]
registries:
pypi:
url: "https://pypi.org/simple/"
token_env: "PYPI_TOKEN"
testpypi:
url: "https://test.pypi.org/simple/"
token_env: "TEST_PYPI_TOKEN"
strategies:
python:
test: "pytest -xvs --cov"
build: "python -m build"
publish: |
if [ "$ENVIRONMENT" = "production" ]; then
twine upload dist/*
else
twine upload --repository testpypi dist/*
fi
hooks:
post_push: |
curl -X POST "$SLACK_WEBHOOK" \
-d "text='Published my-package v$VERSION'"
project:
name: "fullstack-app"
type: ["python", "nodejs"]
registries:
pypi:
url: "https://pypi.org/simple/"
token_env: "PYPI_TOKEN"
npm:
url: "https://registry.npmjs.org/"
token_env: "NPM_TOKEN"
strategies:
python:
test: "cd backend && pytest"
build: "cd backend && python -m build"
publish: "cd backend && twine upload dist/*"
nodejs:
test: "cd frontend && npm test"
build: "cd frontend && npm run build"
publish: "cd frontend && npm publish"