├── CHANGELOG.md ├── Dockerfile ├── Dockerfile.build ├── LICENSE ├── README.md ├── Rakefile ├── bin └── samus ├── commands ├── build │ ├── archive-git-full │ ├── archive-git-full.help.md │ ├── archive-tgz │ ├── archive-tgz.help.md │ ├── archive-zip │ ├── archive-zip.help.md │ ├── changelog-parse │ ├── changelog-parse.help.md │ ├── changelog-rotate │ ├── changelog-rotate.help.md │ ├── chmod-files │ ├── chmod-files.help.md │ ├── fs-copy │ ├── fs-copy.help.md │ ├── fs-mkdir │ ├── fs-mkdir.help.md │ ├── fs-rmrf │ ├── fs-rmrf.help.md │ ├── fs-sedfiles │ ├── fs-sedfiles.help.md │ ├── gem-build │ ├── gem-build.help.md │ ├── git-archive │ ├── git-archive.help.md │ ├── git-clone │ ├── git-clone.help.md │ ├── git-commit │ ├── git-commit.help.md │ ├── git-merge │ ├── git-merge.help.md │ ├── make-task │ ├── make-task.help.md │ ├── npm-pack │ ├── npm-pack.help.md │ ├── npm-task │ ├── npm-task.help.md │ ├── npm-test │ ├── npm-test.help.md │ ├── rake-task │ ├── rake-task.help.md │ ├── ruby-bundle │ ├── ruby-bundle.help.md │ ├── samus-build │ ├── samus-build.help.md │ └── support │ │ └── generate-commit-message.rb └── publish │ ├── cf-invalidate │ ├── cf-invalidate.help.md │ ├── gem-push │ ├── gem-push.help.md │ ├── git-push │ ├── git-push.help.md │ ├── github-release │ ├── github-release.help.md │ ├── npm-publish │ ├── npm-publish.help.md │ ├── s3-put │ ├── s3-put.help.md │ ├── samus-publish │ └── samus-publish.help.md ├── entrypoint.sh ├── lib ├── samus.rb └── samus │ ├── action.rb │ ├── build_action.rb │ ├── builder.rb │ ├── command.rb │ ├── credentials.rb │ ├── publish_action.rb │ ├── publisher.rb │ ├── rake │ └── tasks.rb │ └── version.rb ├── samus.gemspec └── samus.json /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # main 2 | 3 | # 3.0.9 - June 19th, 2020 4 | 5 | [3.0.9]: https://github.com/lsegal/samus/compare/v3.0.8...v3.0.9 6 | 7 | - Improved support for alternate primary branch names 8 | 9 | # 3.0.8 - April 1st, 2019 10 | 11 | [3.0.8]: https://github.com/lsegal/samus/compare/v3.0.7...v3.0.8 12 | 13 | - Fix timezone handling issue. 14 | 15 | # 3.0.7 - April 1st, 2019 16 | 17 | [3.0.7]: https://github.com/lsegal/samus/compare/v3.0.6...v3.0.7 18 | 19 | - Fix bug in `publish` task of DockerReleaseTask in previous release. 20 | 21 | # 3.0.6 - April 1st, 2019 22 | 23 | [3.0.6]: https://github.com/lsegal/samus/compare/v3.0.5...v3.0.6 24 | 25 | - Add `--skip-restore` to samus build to skip restoring Git repository. Useful 26 | with Docker build support in order to inspect output of a built release. 27 | - Add `build/changelog-rotate` command for changelog rotation. 28 | - Add `inspect` and `clean` Rake tasks for `DockerReleaseTask` to inspect and 29 | remove a previously built release respectively. 30 | 31 | # 3.0.5 - April 1st, 2019 32 | 33 | - Fix bug that breaks DockerReleaseTask if .gitconfig or .samus configs are 34 | not present on the system. 35 | 36 | # 3.0.4 - April 1st, 2019 37 | 38 | - Automatically build Dockerfile.samus as tempfile if it is not present in 39 | the repo when using `Samus::Rake::DockerReleaseTask`. This docker image 40 | copies all credentials in so it can be run directly without mounts. 41 | - Add `mount_samus_config` option (defaults to `false`) to `DockerReleaseTask` 42 | options to allow Docker image to mount the Samus configuration directory 43 | from the host when publishing the image. To override the config directory, 44 | specify the `SAMUS_CONFIG_PATH` environment variable to the `publish` task. 45 | - Add `extra_config` to `DockerReleaseTask` to allow extra files to be 46 | copied into the `/root` directory of the build image. The value should be 47 | a hash of src -> dest filenames to copy. 48 | 49 | # 3.0.3 - April 1st, 2019 50 | 51 | - Add `Samus::Rake::ReleaseTask` and `Samus::Rake::DockerReleaseTask` to 52 | generate helpful Rake tasks to generate releases. Example: 53 | 54 | ```ruby 55 | require 'samus' 56 | 57 | Samus::Rake::ReleaseTask.new do |t| 58 | t.git_pull_after_release = true # default is true 59 | t.zipfile = "customzip.tar.gz" # default release-vX.Y.Z.tar.gz 60 | t.buildfile = "samus.json" # default is samus.json 61 | end 62 | ``` 63 | 64 | - Add `lsegal/samus:build` Dockerfile to simplify creation of build docker images. 65 | 66 | # 3.0.2 - April 1st, 2019 67 | 68 | - Add `chmod-files` command to fix file permissions on globs. 69 | 70 | # 3.0.1 - March 30th, 2019 71 | 72 | - Fix bug in `publish/github-release` command due to invalid tag handling. 73 | 74 | # 3.0.0 - March 30th, 2019 75 | 76 | - Add `build/ruby-bundle` command to run Bundler commands like install. 77 | - Update `build/rake-task` to `bundle exec` when a Gemfile is present. 78 | - Update `lsegal/samus` Docker image to contain Bundler 1.17.2. 79 | - Update `build/changelog-parse` to support different formatting. 80 | - Update `build/git-merge` to no longer pull from remote since credentials 81 | are not supported at build time. 82 | 83 | # 2.0.3 - August 12th, 2018 84 | 85 | - Add `--docker` support to build and publish which runs Samus inside a pre-built 86 | container with all default dependencies. You can provide 87 | `--docker-image image-name` to use a different image from the default 88 | `lsegal/samus` container. 89 | - Fix `changelog-parse` command. 90 | 91 | # 2.0.2 - August 11th, 2018 92 | 93 | - Some more fixes for Windows compatibility when using `archive-git-full`. 94 | 95 | # 2.0.0 - August 9th, 2018 96 | 97 | - Add support for Windows. This caused a backwards incompatible change where 98 | environment variables are now UPPERCASED by default. In general this should 99 | have no effect if you rely only on built-in scripts. 100 | - Report an error if credentials cannot be parsed. 101 | 102 | # 1.6.0 - July 19, 2018 103 | 104 | - Add support for credentials for git-push. Add a credentials file with 105 | an RSA key in the format `Key: ...RSA KEY HERE...`. 106 | - Add experimental support for publishing via `lsegal/samus` Docker image. Use 107 | `samus publish --docker project-vX.Y.Z.tar.gz` to perform commands in a 108 | Docker image with the base support for all default publish commands. You must 109 | have Docker installed to use this flag. 110 | 111 | # 1.4.3 - May 19, 2014 112 | 113 | - Add `build/make-task` command to run a make task. 114 | 115 | # 1.4.2 - October 26, 2014 116 | 117 | - Add `build/changelog-parse` command to build ChangeLog from latest entries. 118 | 119 | # 1.4.1 - October 24, 2014 120 | 121 | - Remove date from title in `publish/github-release` 122 | 123 | # 1.4.0 - October 24, 2014 124 | 125 | - Add `publish/github-release` command. 126 | 127 | # 1.3.0 - July 23, 2014 128 | 129 | - Fix issue where repository would not reset when using `samus-build` command. 130 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | 3 | RUN apk add -U --no-cache openssh ruby ruby-json nodejs git curl 4 | RUN pip install awscli 5 | RUN gem install bundler --no-document 6 | RUN gem install rake --no-document 7 | RUN mkdir -p ~/.ssh 8 | RUN echo "Host *" > ~/.ssh/config 9 | RUN echo " StrictHostKeyChecking no" >> ~/.ssh/config 10 | RUN chmod 400 ~/.ssh/config 11 | 12 | RUN git config --global user.email "bot@not.human" 13 | RUN git config --global user.name "Samus Release Bot" 14 | 15 | COPY . /samus 16 | RUN chmod 755 /samus/commands/build/* /samus/commands/publish/* 17 | ENV PATH=$PATH:/samus/bin 18 | 19 | WORKDIR /build 20 | ENTRYPOINT [ "/samus/entrypoint.sh" ] 21 | -------------------------------------------------------------------------------- /Dockerfile.build: -------------------------------------------------------------------------------- 1 | FROM lsegal/samus:latest 2 | ENTRYPOINT samus publish release-v${VERSION}.tar.gz 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Loren Segal 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Samus Gem Version [![Code Climate](https://codeclimate.com/github/lsegal/samus.png)](https://codeclimate.com/github/lsegal/samus) 2 | 3 | Samus helps you automate the release of Open Source Software. Samus works 4 | through a manifest file to describe discrete steps that you typically perform 5 | when packaging and publishing code. 6 | 7 | Samus comes with a set of built-in commands that let you prepare your 8 | repository, push your changes, package your library, and upload it to various 9 | locations / package managers. Samus is also open-source, so you can contribute 10 | new commands if you think they are useful to others. Finally, Samus allows you 11 | to install and share custom commands and credentials for building and publishing 12 | your code. That's right, Samus has a mechanism to share publishing credentials 13 | in a fairly secure way, so you can reliably publish releases from almost any 14 | machine. 15 | 16 | ## Installing 17 | 18 | Samus is a RubyGem and requires Ruby 1.9.x+. Installing is as easy as typing: 19 | 20 | ```sh 21 | gem install samus 22 | ``` 23 | 24 | If you would rather use Samus via Docker, see the Docker section in Usage below. 25 | 26 | ## Usage 27 | 28 | Samus is driven by a manifest file that describes the steps to perform when 29 | building or publishing a release. You can just use Samus to publish, or you 30 | can use it for both, it's your choice. 31 | 32 | ### Publishing 33 | 34 | If you can handle building all of your assets on your own, you can use Samus 35 | just to publish your code. Create a manifest file called `manifest.json` (it 36 | must be named this way) and put it in a directory with all of your assets. The 37 | manifest file is just a list of discrete actions like so (minus comments): 38 | 39 | ```js 40 | { 41 | "actions": [ 42 | { 43 | "files": "git.tgz", // this is an archive of your git repository 44 | "action": "git-push", 45 | "arguments": { 46 | "remotes": "origin", 47 | "refs": "main v1.5.0" // the v1.5.0 is a tag for your release 48 | } 49 | }, 50 | { 51 | "action": "gem-push", 52 | "files": ["my-built-gemfile.gem"], 53 | "credentials": "my-credentials-key" 54 | } 55 | ] 56 | } 57 | ``` 58 | 59 | Note: The credentials section defines a flat file or executable Samus looks 60 | at to get your key for authentication. See the "Custom Commands & Credentials" 61 | section below for how to point to this file. 62 | 63 | Now just run `samus publish .`, and Samus will run these commands in order, 64 | pushing your Git repository and your RubyGem to the world. 65 | 66 | ### Building 67 | 68 | In most cases you will want some help staging a release; Samus can help with 69 | that too. Just in the same way you created a manifest for publishing, you 70 | create a manifest file for building your release. The only difference is now 71 | you include build-time actions, in addition to your publish actions. 72 | 73 | Here is an example that updates your version.rb file, commits and tags the 74 | release, and zips up your repository and RubyGem for publishing. Call 75 | it "samus.json" for easier integration: 76 | 77 | ```js 78 | // samus.json: 79 | { 80 | "actions": [ 81 | { 82 | "action": "fs-sedfiles", 83 | "files": ["lib/my-gem/version.rb"], 84 | "arguments": { 85 | "search": "VERSION = ['\"](.+?)['\"]", 86 | "replace": "VERSION = '$version'" 87 | } 88 | }, 89 | { 90 | "action": "git-commit", 91 | "files": ["lib/my-gem/version.rb"] 92 | }, 93 | { 94 | "action": "git-merge", // merge new commit into main branch 95 | "arguments": { 96 | "branch": "main" 97 | } 98 | }, 99 | { 100 | "action": "archive-git-full", 101 | "files": ["git.tgz"], 102 | "publish": [{ 103 | "action": "git-push", 104 | "arguments": { 105 | "remotes": "origin", 106 | "refs": "main v$version" 107 | } 108 | }] 109 | }, 110 | { 111 | "action": "gem-build", 112 | "files": ["my-gem.gemspec"], 113 | "publish": [ 114 | { 115 | "action": "gem-push", 116 | "files": ["my-gem-$version.gem"], 117 | "credentials": "my-credentials-key" 118 | } 119 | ] 120 | } 121 | ] 122 | } 123 | ``` 124 | 125 | It looks a little longer, but it contains all of the steps to automate when 126 | bumping the VERSION constant, tagging a version, merging into the primary 127 | branch, and building the gem. To build a release with this manifest, simply 128 | type: 129 | 130 | ```sh 131 | samus build 1.5.0 132 | ``` 133 | 134 | Samus will look for `samus.json` and build a release for version 1.5.0 of your 135 | code. It will produce an archive called `release-v1.5.0.tar.gz` that you 136 | can then publish with: 137 | 138 | ```sh 139 | samus publish release-v1.5.0.tar.gz 140 | ``` 141 | 142 | You may have noticed some funny looking "\$version" strings in the above 143 | manifest. Those strings will be replaced with the version provided in the 144 | build command, so all the correct tagging and building will be handled for you. 145 | 146 | You will also notice that the publish commands are part of this manifest. 147 | In build mode, Samus handles building of the manifest.json document, grabbing 148 | any of the "publish" sections of an action and throwing them in the final 149 | manifest.json. As illustrated above, not all actions require a publish section. 150 | If you want to inspect the manifest file that Samus created, you can build 151 | your release as a directory instead of a zip with `--no-zip`. 152 | 153 | Note: If you didn't name your manifest samus.json you can simply enter the 154 | filename in the build command as `samus build VERSION manifest.json`. 155 | 156 | ### Docker Support 157 | 158 | If you would prefer to run Samus on a pre-built image with prepared 159 | dependencies, you can use the 160 | [lsegal/samus](https://hub.docker.com/r/lsegal/samus/) Docker image as follows: 161 | 162 | ```sh 163 | docker run --rm -v $HOME:/root -w /root/${PWD#$HOME} -it lsegal/samus \ 164 | samus build 165 | ``` 166 | 167 | Remember to replace `` with your version string (i.e. `1.0.0`). Then 168 | to publish, use: 169 | 170 | ```sh 171 | docker run --rm -v $HOME:/root:ro -w /root/${PWD#$HOME} -it lsegal/samus \ 172 | samus publish release-v.tar.gz 173 | ``` 174 | 175 | #### Docker Isolation Notes 176 | 177 | Note that these instructions are _not_ meant to run an isolated release 178 | environment, but instead as a convenience to provide all of the non-Ruby 179 | dependencies that Samus might need. If you wish to build and deploy from an 180 | isolated environment, you would have to build a Dockerfile `FROM lsegal/samus` 181 | and ensure that all necessary credentials and configuration is copied in. This 182 | is an exercise left up to the user, since it can be complex and depends on the 183 | amount of configuration needed for building (Git configuration, SSH keys, etc). 184 | 185 | Also note that this syntax is currently only supported for POSIX style systems 186 | and does not yet support Windows. 187 | 188 | ## Built-in Commands 189 | 190 | Samus comes with a number of built-in commands optimized for dealing with 191 | the Git workflow. You can use `samus show-cmd` to list all available commands, 192 | both for building and publishing a release. Each command has documentation 193 | for which files and arguments it accepts. 194 | 195 | ```sh 196 | $ samus show-cmd 197 | ... a list of commands ... 198 | ``` 199 | 200 | To view a specific command, make sure to include the stage (`build` or 201 | `publish`): 202 | 203 | ```sh 204 | $ samus show-cmd publish git-push 205 | Publish Command: git-push 206 | 207 | Pushes Git repository refs to a set of remotes. 208 | 209 | Files: 210 | * The repository archive filename. 211 | 212 | Arguments: 213 | * refs: a space delimited set of commits, branches, or tags to push. 214 | * remotes: a space delimited set of remotes to push refs to. 215 | ``` 216 | 217 | ## Custom Commands & Credentials 218 | 219 | Sometimes you will need to create custom commands that do specific things 220 | for your project. If they are generic, you should submit them to this project, 221 | but if not, you can install custom commands that only you have access to. 222 | This goes for credentials too, which you can install privately on your 223 | machine. 224 | 225 | Samus works best when custom packages are Git-backed (preferably private) 226 | repositories. In this case, you can simply type `samus install REPO` to 227 | download the repository to your machine: 228 | 229 | ```sh 230 | samus install git@github.com:my_org/samus_config 231 | ``` 232 | 233 | Of course, Samus doesn't need these custom packages to be Git-backed. All 234 | the above command does is clone a repository into the ~/.samus directory. 235 | The above command creates: 236 | 237 | ```plaintext 238 | .samus/ 239 | `- samus_config/ 240 | `- commands/ 241 | `- build/ 242 | `- my-command 243 | `- credentials/ 244 | `- my-credentials-key 245 | ``` 246 | 247 | ### Commands 248 | 249 | Commands in Samus are just shell scripts which execute from the workspace 250 | or release directory (unless overridden by the build manifest). Samus passes 251 | all argument values (the keys from the "arguments" section of the manifest) in 252 | as environment variables with a prefixed underscore. For example, the 253 | `rake-task` command is just: 254 | 255 | ```sh 256 | #!/bin/sh 257 | 258 | rake $_TASK 259 | ``` 260 | 261 | The `$_TASK` variable is the "task" argument from the manifest. 262 | 263 | Note that commands must be executable (`chmod +x`) and have proper shebang 264 | lines or they will not function. 265 | 266 | #### Stages 267 | 268 | Commands either live in the build/ or publish/ sub-directories under the 269 | commands directory depending on whether they are for `samus build` or 270 | `samus publish`. These are considered the respective "stages". 271 | 272 | #### Special Variables 273 | 274 | In addition to exposing arguments as underscored environment variables, 275 | Samus also exposes a few special variables with double underscore prefixes: 276 | 277 | - `__BUILD_DIR` - this variable refers to the temporary directory that the 278 | release package is being built inside of. The files inside of this directory, 279 | and _only_ the files inside of this directory, will be built into the release 280 | archive. If you write a build-time command that produces an output file which 281 | is part of the release, you should make sure to move it into this directory. 282 | - `__ORIG_BRANCH` - the original branch being built from. 283 | - `__BUILD_BRANCH` - the name of the branch being built to. 284 | - `__RESTORE_FILE` - the restore file is a newline delimited file containing 285 | branches and their original ref. All branches listed in this file will be 286 | restored to the respective ref at the end of `samus build` regardless of 287 | success status. If you make destructive modifications to existing branches 288 | in the workspace repository, you should add the original ref for the branch 289 | to this file. 290 | - `__CREDS_*` - provides key, secret, and other values loaded from credentials. 291 | See Credentials section for more information on how these are set. 292 | 293 | #### Help Files 294 | 295 | In order to integrate with `samus show-cmd ` syntax, your 296 | command should include a file named `your-command.help.md` in the same directory 297 | as the command script itself. These files are Markdown-formatted files and 298 | should follow the same structure of the built-in command help files: 299 | 300 | ```markdown 301 | Short description of command. 302 | 303 | - Files: 304 | 305 | - Description of what the command line arguments are 306 | 307 | - Arguments: 308 | - argname: Documentation for argument 309 | ``` 310 | 311 | Notes: 312 | 313 | - The first line of the help file is used as the summary in the `show-cmd` 314 | listing. 315 | - Never omit a section. If a command has no files or arguments, use "(none)" 316 | as the list item text. 317 | 318 | ### Credentials 319 | 320 | Custom credentials are just flat files or executables in the `credentials/` 321 | directory of your custom package. When you use the "credentials" section in 322 | a publish action of the manifest, the value should match the filename of 323 | a file in one of your credentials directories. For instance, for the 324 | `my-credentials-key` value in our manifest examples, you should have: 325 | 326 | ``` 327 | .samus/samus_config/credentials/my-credentials-key 328 | ``` 329 | 330 | This file is either a flat file with the format: 331 | 332 | ``` 333 | Key: THE_API_KEY 334 | Secret: THE_SECRET 335 | ``` 336 | 337 | Or, alternatively, an _executable_ which prints the above format to standard 338 | out. 339 | 340 | These values are read in by Samus and get exposed as `$__CREDS_KEY` and 341 | `$__CREDS_SECRET` respectively in Samus commands. You can provide other 342 | metadata as well, which would be included as `$__CREDS_NAME` (for the 343 | line "NAME: value"). 344 | 345 | ## Manifest File Format 346 | 347 | The following section defines the manifest formats for the samus.json build 348 | manifest as well as the manifest.json stored in release packages. 349 | 350 | ### Base Format 351 | 352 | The base format is defined as follows: 353 | 354 | ```js 355 | { 356 | "actions": [ 357 | { 358 | "action": "COMMAND_NAME", // [required] command name to execute 359 | "files": ["file1", ...], // optional list of files 360 | "arguments": { // optional map of arguments to pass to cmd 361 | "key": "value", // each key is passed in as _key in ENV 362 | // ... (optional) more keys ... 363 | }, 364 | "pwd": "path" // optional path to execute command from 365 | "credentials": "KEY", // optional credentials to load for cmd 366 | }, 367 | // ... (optional) more action items ... 368 | ] 369 | } 370 | ``` 371 | 372 | All manifests include a list of "actions", known individually as action items. 373 | Each action item has a single required property, "action", which is the command 374 | to execute for the action (found in `samus show-cmd`). An optional list of 375 | files are passed into the command as command line arguments, and the "arguments" 376 | property is a map of keys to values passed in as environment variables with a 377 | "\_" prefix (key "foo" is set as environment variable "\_foo"). Optional 378 | credentials are loaded from the credentials directory. 379 | 380 | ### Build Manifest Format 381 | 382 | The build manifest format is similar to the above but allows for two extra keys 383 | in each action item called "publish" and "condition". 384 | 385 | #### "publish" Property 386 | 387 | The "publish" property should contain the action item that is added to the 388 | final manifest.json built into the release package if the action item is 389 | evaluated (condition matches and command successfully executes). If a "files" 390 | property is set on the parent action item, that property is copied into the 391 | publish action by default, but it can be overridden. 392 | 393 | Here is an example build manifest showing the added use of the "publish" 394 | property: 395 | 396 | ```js 397 | { 398 | "actions": [ 399 | { 400 | "action": "readme-update", 401 | "files": ["README.txt"], 402 | "publish": { 403 | "action": "readme-publish" 404 | "arguments": { 405 | "host": "www.mywebsite.com" 406 | }, 407 | "credentials": "www.mywebsite.com" 408 | } 409 | }, 410 | { 411 | "action": "readme-build", 412 | "files": ["README.txt"], 413 | "publish": { 414 | "action": "readme-publish" 415 | "arguments": { 416 | "files": ["README.html"], // override files property 417 | "host": "www.mywebsite.com" 418 | }, 419 | "credentials": "www.mywebsite.com" 420 | } 421 | } 422 | ] 423 | 424 | } 425 | ``` 426 | 427 | #### "condition" Property 428 | 429 | The "condition" property is a Ruby expression that is evaluated for truthiness 430 | to decide if the action item should be evaluated or skipped. A common use for 431 | this is to take action based on the version (see "\$version" variable section 432 | below). The following example runs an action item only for version 2.0+ of a 433 | release: 434 | 435 | ```js 436 | { 437 | "action": "rake-task", 438 | "arguments": { "task": "assets:package" }, 439 | "condition": "'$version' > '2.0'" 440 | } 441 | ``` 442 | 443 | #### "\$version" Variable 444 | 445 | A special variable "\$version" is interpolated when loading the build manifest. 446 | This variable can appear anywhere in the JSON document, and is interpolated 447 | before any actions or conditions are evaluated. 448 | 449 | ## Contributing & TODO 450 | 451 | Please help by contributing commands that Samus can use to build or publish 452 | code. Integration with different package managers would be helpful, as well 453 | as improving the kinds of build-time tasks that are exposed. 454 | 455 | ## Copyright 456 | 457 | Samus was created by Loren Segal in 2014 and is available under MIT license. 458 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require_relative './lib/samus' 2 | 3 | task default: 'samus:build' 4 | 5 | Samus::Rake::DockerReleaseTask.new 6 | 7 | task :images do 8 | sh "docker build -t lsegal/samus:latest -f Dockerfile ." 9 | sh "docker build -t lsegal/samus:build -f Dockerfile.build ." 10 | end 11 | 12 | namespace :samus do 13 | task build: :images 14 | end 15 | -------------------------------------------------------------------------------- /bin/samus: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative '../lib/samus' 4 | require 'optparse' 5 | require 'tmpdir' 6 | 7 | command = 8 | case ARGV.shift 9 | when 'install' 10 | Dir.mkdir(Samus::CONFIG_PATH) unless File.directory?(Samus::CONFIG_PATH) 11 | Dir.chdir(Samus::CONFIG_PATH) { system "git clone #{ARGV.shift}" } 12 | exit 13 | when 'update' 14 | Samus.config_paths.each do |path| 15 | Dir.chdir(path) do 16 | if File.directory?('.git') 17 | puts "[I] Updating #{path}" 18 | system 'git pull' 19 | else 20 | puts "[S] Skipping non-Git directory #{path}" 21 | end 22 | end 23 | end 24 | exit 25 | when 'show-cmd' 26 | stage = ARGV.shift 27 | if stage 28 | name = ARGV.shift 29 | if name 30 | Samus::Command.new(stage, name).show_help 31 | else 32 | Samus::Command.list_commands(stage) 33 | end 34 | else 35 | Samus::Command.list_commands 36 | end 37 | exit 38 | when 'publish', 'push' 39 | Samus::Publisher 40 | when 'build' 41 | Samus::Builder 42 | end 43 | 44 | dry_run = false 45 | zip_release = true 46 | skip_restore = false 47 | outfile = nil 48 | docker = false 49 | docker_image = "lsegal/samus:latest" 50 | options = OptionParser.new do |opts| 51 | opts.banner = "Usage: samus publish [options] [release_file ...]\n" 52 | opts.banner += " samus build [options] [build.json]\n" 53 | opts.banner += " samus show-cmd [stage] [name]\n" 54 | 55 | opts.separator '' 56 | opts.separator 'Options:' 57 | opts.on('--dry-run', "Print commands, don't run them") do |_v| 58 | dry_run = true 59 | end 60 | if command == Samus::Builder 61 | opts.on('--[no-]zip', 'Zip release file') do |zip| 62 | zip_release = zip 63 | end 64 | opts.on('-o FILE', '--output', 'The file (no extension) to generate') do |file| 65 | outfile = file 66 | end 67 | opts.on('--skip-restore', 'Skips restore after build completes') do 68 | skip_restore = true 69 | end 70 | end 71 | opts.on('--docker', 'Use Docker to build or publish') do |_v| 72 | docker = true 73 | end 74 | opts.on('--docker-image IMAGE', 'Which Docker image to use (default: lsegal/samus:latest)') do |img| 75 | docker_image = img 76 | end 77 | end 78 | options.parse! 79 | 80 | if docker 81 | cmd = <<-COMMAND 82 | docker run --rm -v "#{Dir.home}:/root" 83 | -v "#{Dir.pwd}:/build" -w /build -it #{docker_image} 84 | sh -c "chmod 400 /root/.ssh/*" && 85 | samus #{command == Samus::Builder ? 'build' : 'publish'} #{ARGV.join(' ')} 86 | COMMAND 87 | cmd = cmd.gsub(/ +/, ' ').delete("\n").strip 88 | puts "[C] #{cmd}" 89 | system(cmd) 90 | exit($?.to_i) 91 | end 92 | 93 | if command == Samus::Publisher 94 | ARGV.each do |dir| 95 | raise "Aborting due to missing path #{dir}" unless File.exist?(dir) 96 | end 97 | 98 | ARGV.each do |dir| 99 | if File.directory?(dir) 100 | command.new(dir).publish(dry_run) 101 | elsif File.file?(dir) # it has to be an archive 102 | Dir.mktmpdir do |tmpdir| 103 | system "tar -xzf #{dir} -C #{tmpdir}" 104 | command.new(tmpdir).publish(dry_run) 105 | end 106 | end 107 | end 108 | elsif command == Samus::Builder 109 | ver = ARGV.shift 110 | raise 'Missing version' if ver.nil? 111 | Samus::Builder.build_version = ver.sub(/^v/, '') 112 | 113 | (ARGV.empty? ? ['samus.json'] : ARGV).each do |file| 114 | command.new(file).build(dry_run, zip_release, outfile, skip_restore) 115 | end 116 | else 117 | puts options 118 | exit 1 119 | end 120 | -------------------------------------------------------------------------------- /commands/build/archive-git-full: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | git gc # might as well GC before archiving .git 5 | tar cfz "$__BUILD_DIR/$1" .git $(git ls-files) 6 | -------------------------------------------------------------------------------- /commands/build/archive-git-full.help.md: -------------------------------------------------------------------------------- 1 | Archives a complete Git repository (including .git directory). 2 | 3 | Files: 4 | * The name of the archived repository. 5 | 6 | Arguments: 7 | * (none) -------------------------------------------------------------------------------- /commands/build/archive-tgz: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | tar cfz $__BUILD_DIR/$1 $_PATHS 4 | -------------------------------------------------------------------------------- /commands/build/archive-tgz.help.md: -------------------------------------------------------------------------------- 1 | Archives a set of paths into a gzipped tar file. 2 | 3 | Files: 4 | * The name of the archive to build. 5 | 6 | Arguments: 7 | * paths: a space delimited set of paths to add to the archive. 8 | -------------------------------------------------------------------------------- /commands/build/archive-zip: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | zip -qr $__BUILD_DIR/$1 $_PATHS 4 | -------------------------------------------------------------------------------- /commands/build/archive-zip.help.md: -------------------------------------------------------------------------------- 1 | Archives a set of paths into a zip file. 2 | 3 | Files: 4 | * The name of the archive to build. 5 | 6 | Arguments: 7 | * paths: a space delimited set of paths to add to the archive. 8 | -------------------------------------------------------------------------------- /commands/build/changelog-parse: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | def parse_changelog(changelog) 4 | log = File.read(changelog) 5 | match = /^#\s*\[?#{ENV["_VERSION"]}\]?\s+-\s+(?.+?)\r?\n(?<body>.*?)\r?\n#/ms.match(log) 6 | match ? match["body"] : nil 7 | end 8 | 9 | file = ARGV[0] 10 | dest = File.join(ENV["__BUILD_DIR"], ENV["_DESTINATION"] || file) 11 | out = parse_changelog(file) 12 | if out 13 | puts "Changelog:" 14 | puts out 15 | File.open(dest, "w") {|f| f.puts(out) } 16 | else 17 | puts "Failed to find changelog data" 18 | exit 1 19 | end 20 | -------------------------------------------------------------------------------- /commands/build/changelog-parse.help.md: -------------------------------------------------------------------------------- 1 | Parses a ChangeLog file and extracts entries for latest release. 2 | 3 | Files: 4 | * The path to the ChangeLog file. 5 | 6 | Arguments: 7 | * destination: an optional destination filename. 8 | -------------------------------------------------------------------------------- /commands/build/changelog-rotate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | file = ARGV[0] 4 | ver = ENV['_VERSION'] 5 | heading = ENV['_HEADING'] || ENV['_MASTER'] || ENV['__ORIG_BRANCH'] 6 | title_fmt = ENV['_TITLE_FORMAT'] || "[$version] - %B %-d$day_nth, %Y" 7 | ENV['TZ'] ||= ENV['_TZ'] 8 | 9 | content = File.read(file) 10 | compare_url = `git config remote.origin.url`.strip.sub(/^git@(.+?):/, 'https://\1/') 11 | day_nth = {1 => 'st', 2 => 'nd', 3 => 'rd'}[Time.now.day % 10] || 'th' 12 | title = Time.now.strftime(title_fmt).sub('$version', ver).sub('$day_nth', day_nth) 13 | 14 | match = /\A\s*# #{heading}\r?\n(?<body>.*?)(?<rest>\r?\n# .+|\Z)/mis.match(content) 15 | raise "Failed to rotate changelog: #{file} (invalid heading: #{heading})" unless match 16 | 17 | prev_ver = match['rest'][/(\d+\.\d+(?:\.\d+)?)/, 1] 18 | repl = "# #{heading}\n\n# #{title}\n\n" + 19 | (prev_ver ? "[#{ver}]: #{compare_url}/compare/v#{prev_ver}...v#{ver}\n" : '') + 20 | match['body'] + match['rest'] 21 | File.open(file, 'w') {|f| f.write(repl) } 22 | puts "Added new #{file} header: #{title}" 23 | puts "Previous #{file} version: #{prev_ver.inspect}" 24 | -------------------------------------------------------------------------------- /commands/build/changelog-rotate.help.md: -------------------------------------------------------------------------------- 1 | Rotates the latest ChangeLog entries into an arbitrary formatted title heading. 2 | The heading is formatted via `title_format` and can include the version 3 | and release date. 4 | 5 | Files: 6 | 7 | - The path to the ChangeLog file. 8 | 9 | Arguments: 10 | 11 | - heading: (optional) defaults to primary branch name, should match the first development 12 | heading used for in-flux changelog entries before rotation. 13 | - title_format: (optional) a `Time.strftime` date formatted string that can 14 | also include `$version` to represent the title of the rotated changelog entry. 15 | Example: `$version - %B %-d, %Y`. It is recommended to put the version at the 16 | front of the title to improve the reliability of generating a compare URL. 17 | - tz: (optional) set the timezone if not available on the build system. Defaults 18 | to system timezone. 19 | -------------------------------------------------------------------------------- /commands/build/chmod-files: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | dir_mask = ENV['_DIR_MASK'] || "775" 4 | entries = {} 5 | 6 | ARGV.each do |arg| 7 | wildcard, want_fmode = *arg.split(',') 8 | Dir.glob(wildcard).each do |entry| 9 | stat = File.stat(entry) 10 | real_mode = stat.mode.to_s(8)[-3..-1] 11 | want_mode = stat.directory? ? dir_mask : want_fmode 12 | 13 | if real_mode != want_mode 14 | entries[entry] = { want: want_mode, real: real_mode } 15 | else 16 | entries.delete(entry) 17 | end 18 | end 19 | end 20 | 21 | entries.each do |f, mode| 22 | File.chmod(mode[:want].to_i(8), f) 23 | puts "Fix permissions: #{mode[:real]} -> #{mode[:want]} for #{f}" 24 | end 25 | -------------------------------------------------------------------------------- /commands/build/chmod-files.help.md: -------------------------------------------------------------------------------- 1 | Corrects permissions for a set of globs. Pass arguments in files 2 | with comma separating glob from permission bits. This command 3 | auto-corrects directories to 775. Set dir_mask to override the 4 | default mask value. 5 | 6 | Example: 7 | 8 | ```json 9 | "files": "bin/**/*,755 lib/**/*.rb,644" 10 | ``` 11 | 12 | Files: 13 | 14 | - A list of globs and permissions separated by a comma, ex: 15 | `**/*.rb,644 **/*.sh,755` 16 | 17 | Arguments: 18 | 19 | - dir_mask: (optional) the directory umask to set. Defaults to 775. 20 | -------------------------------------------------------------------------------- /commands/build/fs-copy: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cp -r $* $__BUILD_DIR/$_DESTINATION 4 | -------------------------------------------------------------------------------- /commands/build/fs-copy.help.md: -------------------------------------------------------------------------------- 1 | Copies a set of files into the build directory. 2 | 3 | Files: 4 | * A list of files to copy into the build directory. 5 | 6 | Arguments: 7 | * destination: (optional) a subdirectory within the build directory to copy 8 | files to. -------------------------------------------------------------------------------- /commands/build/fs-mkdir: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p $__BUILD_DIR/$* 4 | -------------------------------------------------------------------------------- /commands/build/fs-mkdir.help.md: -------------------------------------------------------------------------------- 1 | Creates a sub-directory in the build directory. 2 | 3 | Files: 4 | * The subdirectory to create. Can be a complex directory. 5 | 6 | Arguments: 7 | * (none) -------------------------------------------------------------------------------- /commands/build/fs-rmrf: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm -rf $* 4 | -------------------------------------------------------------------------------- /commands/build/fs-rmrf.help.md: -------------------------------------------------------------------------------- 1 | Removes a set of files from the build directory. 2 | 3 | Files: 4 | * A list of files relative to the build directory to delete. 5 | 6 | Arguments: 7 | * (none) 8 | -------------------------------------------------------------------------------- /commands/build/fs-sedfiles: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ARGV.map {|f| Dir.glob(f) }.flatten.each do |file| 4 | contents = File.read(file) 5 | contents = contents.gsub(Regexp.new(ENV['_SEARCH']), ENV['_REPLACE']) 6 | File.open(file, 'w') {|f| f.write(contents) } 7 | end 8 | -------------------------------------------------------------------------------- /commands/build/fs-sedfiles.help.md: -------------------------------------------------------------------------------- 1 | Replaces text in a list of files in the working copy. 2 | 3 | Files: 4 | * A list of files to perform text replacement on. 5 | 6 | Arguments: 7 | * search: a regular expression to search for. 8 | * replace: the text to replace. 9 | -------------------------------------------------------------------------------- /commands/build/gem-build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | ruby -S gem build $1 5 | mv ${1%.gemspec}-$_VERSION.gem $__BUILD_DIR -------------------------------------------------------------------------------- /commands/build/gem-build.help.md: -------------------------------------------------------------------------------- 1 | Builds a RubyGem package (.gem file) 2 | 3 | Files: 4 | * The name of the gemspec file to build 5 | 6 | Arguments: 7 | * (none) 8 | -------------------------------------------------------------------------------- /commands/build/git-archive: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | git archive --prefix="$_PREFIX" -o $__BUILD_DIR/$1 ${_REF-HEAD} 4 | -------------------------------------------------------------------------------- /commands/build/git-archive.help.md: -------------------------------------------------------------------------------- 1 | Exports the Git repository using the git archive command. 2 | 3 | Files: 4 | * The filename of the exported Git archive. 5 | 6 | Arguments: 7 | * ref: a commit, branch, or tag to export. 8 | * prefix: a prefix to append to every path in the exported archive. 9 | -------------------------------------------------------------------------------- /commands/build/git-clone: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | cd ${_REPOSITORY-.} 6 | BRANCHES=`git branch --list --format="%(refname:lstrip=2)"` 7 | 8 | git clone -q $([ -n "$_BRANCH" ] && echo --branch $_BRANCH) \ 9 | ${_REPOSITORY-.} $__BUILD_DIR/$1 2>${__DEVNULL-/dev/null} 10 | 11 | oldpw=`pwd` 12 | cd $__BUILD_DIR/$1 13 | for branch in $BRANCHES; do 14 | echo Copying over $branch from repository. 15 | git branch $branch --track origin/$branch 2>${__DEVNULL-/dev/null} || true 16 | done 17 | cd $oldpw 18 | 19 | # preserve all previously configured remotes for publish actions 20 | cp ${_REPOSITORY-.}/.git/config $__BUILD_DIR/$1/.git/config 21 | -------------------------------------------------------------------------------- /commands/build/git-clone.help.md: -------------------------------------------------------------------------------- 1 | Clones the Git working copy into the build directory. 2 | 3 | Files: 4 | * The path inside the build directory to clone into. 5 | 6 | Arguments: 7 | * repository: the location of the working copy repository, defaults to ".". 8 | * branch: the branch to clone, clones to default primary if not provided. 9 | -------------------------------------------------------------------------------- /commands/build/git-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | IFS='%' 5 | 6 | message=$($(dirname $0)/support/generate-commit-message.rb) 7 | 8 | git add $* 9 | git commit -m "$message" 10 | git tag ${_TAG-v$_VERSION} 11 | echo tag ${_TAG-v$_VERSION} >> $__RESTORE_FILE 12 | -------------------------------------------------------------------------------- /commands/build/git-commit.help.md: -------------------------------------------------------------------------------- 1 | Performs a commit on the working copy repository and tags the commit. 2 | 3 | Files: 4 | * The list of files to stage for commit. 5 | 6 | Arguments: 7 | * message: the commit message, defaults to "Bump version to vVERSION". 8 | * tag: the tag name, defaults to vVERSION. 9 | -------------------------------------------------------------------------------- /commands/build/git-merge: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | git checkout -q $_BRANCH 5 | echo branch $_BRANCH $(git rev-parse HEAD) >> $__RESTORE_FILE 6 | git rebase ${_REMOTE-origin}/$_BRANCH 7 | git merge $__BUILD_BRANCH -q -m "Merge release branch into $_BRANCH" -s recursive -Xtheirs --ff 8 | git checkout -q $__BUILD_BRANCH 9 | -------------------------------------------------------------------------------- /commands/build/git-merge.help.md: -------------------------------------------------------------------------------- 1 | Merges release branch into a destination branch. 2 | 3 | Files: 4 | * (none) 5 | 6 | Arguments: 7 | * branch: the destination branch to merge. 8 | * remote: the remote to fetch before merging. Defaults to "origin". 9 | -------------------------------------------------------------------------------- /commands/build/make-task: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | make $_TASK 4 | -------------------------------------------------------------------------------- /commands/build/make-task.help.md: -------------------------------------------------------------------------------- 1 | Runs a Make task. 2 | 3 | Files: 4 | * (none) 5 | 6 | Arguments: 7 | * task: the task to execute. 8 | -------------------------------------------------------------------------------- /commands/build/npm-pack: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | mv `npm pack` $__BUILD_DIR/$1 5 | -------------------------------------------------------------------------------- /commands/build/npm-pack.help.md: -------------------------------------------------------------------------------- 1 | Builds an npm package. 2 | 3 | Files: 4 | * The output name of the npm package (with .tgz suffix) 5 | 6 | Arguments: 7 | * (none) 8 | -------------------------------------------------------------------------------- /commands/build/npm-task: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npm $_TASK $* 4 | -------------------------------------------------------------------------------- /commands/build/npm-task.help.md: -------------------------------------------------------------------------------- 1 | Runs a npm task. 2 | 3 | Files: 4 | * An optional list of files to run task on. 5 | 6 | Arguments: 7 | * task: the task to execute. 8 | -------------------------------------------------------------------------------- /commands/build/npm-test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npm test 4 | -------------------------------------------------------------------------------- /commands/build/npm-test.help.md: -------------------------------------------------------------------------------- 1 | Runs npm test on the working directory. 2 | 3 | Files: 4 | * (none) 5 | 6 | Arguments: 7 | * (none) 8 | -------------------------------------------------------------------------------- /commands/build/rake-task: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -f Gemfile ]; then 4 | ruby -S bundle exec rake $_TASK 5 | else 6 | ruby -S rake $_TASK 7 | fi 8 | -------------------------------------------------------------------------------- /commands/build/rake-task.help.md: -------------------------------------------------------------------------------- 1 | Runs a Rake task. 2 | 3 | Files: 4 | * (none) 5 | 6 | Arguments: 7 | * task: the task to execute. 8 | -------------------------------------------------------------------------------- /commands/build/ruby-bundle: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ruby -S bundle ${_COMMAND:-install} 4 | -------------------------------------------------------------------------------- /commands/build/ruby-bundle.help.md: -------------------------------------------------------------------------------- 1 | Runs `bundler` on a given directory 2 | 3 | Arguments: 4 | * command: the Bundler command to run 5 | -------------------------------------------------------------------------------- /commands/build/samus-build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | bakfile=${__RESTORE_FILE}.$$.$RANDOM.bak 5 | mv $__RESTORE_FILE $bakfile 6 | trap "mv $bakfile $__RESTORE_FILE" 0 7 | ruby -S samus build -o $__BUILD_DIR/$1 $_BUILD_VERSION ${_MANIFEST} 8 | -------------------------------------------------------------------------------- /commands/build/samus-build.help.md: -------------------------------------------------------------------------------- 1 | Builds a separate Samus-backed repository as part of this Samus build. 2 | 3 | Files: 4 | * The archive name of the generated Samus release package. 5 | 6 | Arguments: 7 | * build_version: the version of the package to build. 8 | * manifest: (optional) the name of the manifest to use when building the 9 | manifest. 10 | -------------------------------------------------------------------------------- /commands/build/support/generate-commit-message.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | def word_wrap(text) 4 | text.gsub(/(.{1,60})(\s|$)/, "\\1\n ") 5 | end 6 | 7 | def collect_issues 8 | out = `git log $(git describe --tags --abbrev=0)...HEAD -E --grep '#[0-9]+' 2>#{ENV['__DEVNULL']}` 9 | issues = out.scan(/((?:\S+\/\S+)?#\d+)/).flatten 10 | end 11 | 12 | message = ENV["_MESSAGE"] 13 | if message.nil? || message.strip.empty? 14 | message = "Tag release v#{ENV["_VERSION"]}" 15 | 16 | issues = collect_issues 17 | if issues.size > 0 18 | message += "\n\nReferences:\n " + word_wrap(issues.uniq.sort.join(", ")) 19 | end 20 | end 21 | 22 | puts message 23 | -------------------------------------------------------------------------------- /commands/publish/cf-invalidate: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export AWS_ACCESS_KEY_ID=$__CREDS_KEY 4 | export AWS_SECRET_ACCESS_KEY=$__CREDS_SECRET 5 | 6 | qty="$#" 7 | items=`python -c 'import sys, json; print json.dumps(sys.argv[1:])' $*` 8 | ref=`date` 9 | 10 | aws configure set preview.cloudfront true 11 | aws cloudfront create-invalidation \ 12 | --region $_REGION \ 13 | --distribution-id $_DISTRIBUTION \ 14 | --invalidation-batch "{\"Paths\":{\"Quantity\":$qty,\"Items\":$items},\"CallerReference\":\"$ref\"}" 15 | -------------------------------------------------------------------------------- /commands/publish/cf-invalidate.help.md: -------------------------------------------------------------------------------- 1 | Invalidates a CloudFront distribution. 2 | 3 | Files: 4 | * A list of URIs to invalidate. 5 | 6 | Arguments: 7 | * distribution: The CloudFront distribution ID. 8 | * region: The region of the CloudFront distribution. 9 | -------------------------------------------------------------------------------- /commands/publish/gem-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | curl --fail --silent -X POST \ 4 | -H "Content-Type: application/octet-stream" \ 5 | -H "Authorization: $__CREDS_SECRET" \ 6 | --data-binary @$* https://rubygems.org/api/v1/gems 7 | echo "" 8 | -------------------------------------------------------------------------------- /commands/publish/gem-push.help.md: -------------------------------------------------------------------------------- 1 | Publishes a RubyGem package (.gem file). 2 | 3 | Files: 4 | * The .gem file to publish. 5 | 6 | Arguments: 7 | * (none) 8 | -------------------------------------------------------------------------------- /commands/publish/git-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | olddir=$(pwd) 5 | dir=$1 6 | 7 | case "$dir" in 8 | *.tgz|*.gz) 9 | dir=$(mktemp -d -t samusXXXX) 10 | trap "rm -rf $dir" 0 11 | tar -xzf $* -C $dir 12 | ;; 13 | esac 14 | 15 | cd $dir 16 | echo "-----BEGIN RSA PRIVATE KEY-----" > .sshkey 17 | echo "$__CREDS_KEY" | fold -w 65 >> .sshkey 18 | echo "-----END RSA PRIVATE KEY-----" >> .sshkey 19 | chmod 400 .sshkey 20 | 21 | for r in $_REMOTES; do 22 | ssh-agent sh -c "ssh-add .sshkey; git push $r $_REFS" 23 | done 24 | cd $olddir 25 | -------------------------------------------------------------------------------- /commands/publish/git-push.help.md: -------------------------------------------------------------------------------- 1 | Pushes Git repository refs to a set of remotes. 2 | 3 | Files: 4 | * The repository archive filename. 5 | 6 | Arguments: 7 | * refs: a space delimited set of commits, branches, or tags to push. 8 | * remotes: a space delimited set of remotes to push refs to. -------------------------------------------------------------------------------- /commands/publish/github-release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "json" 4 | require "net/https" 5 | require "uri" 6 | 7 | def fail(msg, code = 1) 8 | $stderr.puts msg 9 | exit code 10 | end 11 | 12 | def https_request(opts = {}) 13 | uri = URI.parse(opts[:uri]) 14 | key, body = opts[:key], opts[:body] 15 | content_type = opts[:content_type] || "application/json" 16 | method = opts[:method] || :Post 17 | 18 | http = Net::HTTP.new(uri.host, uri.port) 19 | http.use_ssl = true 20 | request = Net::HTTP.const_get(method).new(uri.request_uri) 21 | request["content-type"] = "application/json" 22 | request["authorization"] = "token #{key}" 23 | 24 | if content_type == "application/json" 25 | request.body = body 26 | else 27 | request.body_stream = body 28 | request.content_length = body.size 29 | end 30 | 31 | response = http.request(request) 32 | if response.code[0] != "2" 33 | raise RuntimeError, response.code + " " + response.message 34 | else 35 | response 36 | end 37 | end 38 | 39 | key = ENV["__CREDS_SECRET"] 40 | repository = ENV["_REPOSITORY"] 41 | tag = ENV["_TAG"] 42 | changelog = ENV["_CHANGELOG"] 43 | title = ENV["_TITLE"] || "Release #{tag}" 44 | out_file = ENV["_OUT_FILE"] 45 | assets = ARGV 46 | 47 | fail "Missing `repository`" unless repository 48 | fail "Missing `tag`" unless tag 49 | 50 | begin 51 | uri = "https://api.github.com/repos/#{repository}/releases" 52 | body = JSON.generate({ 53 | "body" => changelog ? File.read(changelog) : "", 54 | "name" => title, 55 | "tag_name" => tag, 56 | }) 57 | response = https_request uri: uri, key: key, body: body 58 | json = JSON.parse(response.body) 59 | 60 | assets.each do |asset| 61 | local_name, remote_name = *asset.split(':', 2) 62 | remote_name ||= local_name 63 | uri = json["upload_url"].sub(/\{\?name,label\}$/, "?name=#{remote_name}") 64 | File.open(local_name, "r") do |file| 65 | begin 66 | https_request uri: uri, key: key, 67 | body: file, content_type: "application/octet-stream" 68 | rescue => e 69 | puts "Failed to upload release asset #{local_name} as #{remote_name}." 70 | https_request uri: json["url"], key: key, method: :Delete, body: "" 71 | raise 72 | end 73 | end 74 | end 75 | 76 | File.open(out_file, "w") {|f| f.write(json["url"]) } if out_file 77 | puts "Created GitHub release of #{repository}@#{tag}" 78 | puts "URL: #{json["url"]}" 79 | rescue => e 80 | fail "Failed to create GitHub release of #{repository}@#{tag}: #{e.message}" 81 | end 82 | -------------------------------------------------------------------------------- /commands/publish/github-release.help.md: -------------------------------------------------------------------------------- 1 | Creates a GitHub release from a given tag. 2 | 3 | Files: 4 | * An optional list of assets to upload with the release. 5 | 6 | Arguments: 7 | * repository: the fully qualified repository name (user/repo). 8 | * tag: the tag name to create the release for. 9 | * title: an optional title for the release. 10 | * changelog: an optional filename containing changelog data. 11 | * out_file: an optional filename to write the resulting release URL to. 12 | -------------------------------------------------------------------------------- /commands/publish/npm-publish: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npm publish --email=$__CREDS_KEY --_auth=$__CREDS_SECRET ./$* 4 | -------------------------------------------------------------------------------- /commands/publish/npm-publish.help.md: -------------------------------------------------------------------------------- 1 | Publishes an npm package. 2 | 3 | Files: 4 | * The filename of the archived npm package 5 | 6 | Arguments: 7 | * (none) 8 | -------------------------------------------------------------------------------- /commands/publish/s3-put: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export AWS_ACCESS_KEY_ID=$__CREDS_KEY 4 | export AWS_SECRET_ACCESS_KEY=$__CREDS_SECRET 5 | 6 | set -e 7 | for f in $*; do 8 | recursive="" 9 | if [ -d $f ]; then 10 | recursive="--recursive" 11 | fi 12 | aws s3 cp $recursive --acl public-read --region $_REGION $f s3://$_BUCKET/$_PREFIX$_KEY 13 | done 14 | -------------------------------------------------------------------------------- /commands/publish/s3-put.help.md: -------------------------------------------------------------------------------- 1 | Uploads files or directories to an Amazon S3 bucket. 2 | 3 | Files: 4 | * A list of files to upload to the S3 bucket. 5 | 6 | Arguments: 7 | * region: the region of the S3 bucket. 8 | * bucket: the name of the S3 bucket. 9 | * prefix: (optional) a path to prefix to the uploaded key names. 10 | * key: (optional) a key prefix path. 11 | -------------------------------------------------------------------------------- /commands/publish/samus-publish: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | samus publish $1 4 | -------------------------------------------------------------------------------- /commands/publish/samus-publish.help.md: -------------------------------------------------------------------------------- 1 | Publishes a Samus release package. 2 | 3 | Files: 4 | * The archive name of the generated Samus release package. 5 | 6 | Arguments: 7 | * (none) 8 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sh -c "/samus/bin/samus $*" 4 | -------------------------------------------------------------------------------- /lib/samus.rb: -------------------------------------------------------------------------------- 1 | require_relative './samus/publisher' 2 | require_relative './samus/builder' 3 | 4 | module Samus 5 | CONFIG_PATH = File.expand_path(ENV['SAMUS_CONFIG_PATH'] || '~/.samus') 6 | 7 | module Rake 8 | # Autoloads 9 | autoload :ReleaseTask, File.expand_path('samus/rake/tasks', __dir__) 10 | autoload :DockerReleaseTask, File.expand_path('samus/rake/tasks', __dir__) 11 | end 12 | 13 | module_function 14 | 15 | def config_paths; @@config_paths end 16 | 17 | @@config_paths = [] 18 | 19 | def load_configuration_directory 20 | if File.exist?(CONFIG_PATH) 21 | Dir.foreach(CONFIG_PATH) do |dir| 22 | next if dir == '.' || dir == '..' 23 | dir = File.join(CONFIG_PATH, dir) 24 | config_paths.unshift(dir) if File.directory?(dir) 25 | end 26 | end 27 | end 28 | 29 | def load_commands 30 | config_paths.each do |path| 31 | path = File.join(path, 'commands') 32 | Samus::Command.command_paths.unshift(path) if File.directory?(path) 33 | end 34 | end 35 | 36 | def error(msg) 37 | puts "[E] #{msg}" 38 | exit(1) 39 | end 40 | 41 | def windows? 42 | ::RbConfig::CONFIG['host_os'] =~ /mingw|win32|cygwin/ ? true : false 43 | end 44 | end 45 | 46 | Samus.load_configuration_directory 47 | Samus.load_commands 48 | -------------------------------------------------------------------------------- /lib/samus/action.rb: -------------------------------------------------------------------------------- 1 | require_relative './command' 2 | require_relative './credentials' 3 | 4 | module Samus 5 | class Action 6 | def initialize(opts = {}) 7 | @raw_options = opts 8 | @dry_run = opts[:dry_run] 9 | @allow_fail = false 10 | @command = nil 11 | @creds = nil 12 | @arguments = opts[:arguments] || {} 13 | end 14 | 15 | def stage 16 | raise NotImplementedError, 'action must define stage' 17 | end 18 | 19 | def load(opts = {}) 20 | opts.each do |key, value| 21 | meth = "#{key}=" 22 | if respond_to?(meth) 23 | send(meth, value) 24 | else 25 | Samus.error("Unknown action property: #{key}") 26 | end 27 | end 28 | self 29 | end 30 | 31 | def run 32 | @command.run(command_options) if @command 33 | end 34 | 35 | def command_options 36 | { 37 | arguments: @creds ? @arguments.merge(@creds.load) : @arguments, 38 | files: @files, 39 | dry_run: @dry_run, 40 | allow_fail: @allow_fail 41 | } 42 | end 43 | 44 | def action=(name) 45 | @command = Command.new(stage, name) 46 | end 47 | 48 | def credentials=(key) 49 | @creds = Credentials.new(key) 50 | end 51 | 52 | def allowFail=(value) 53 | @allow_fail = value 54 | end 55 | 56 | attr_writer :files 57 | 58 | def arguments=(args) 59 | args.each { |k, v| @arguments[k] = v } 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/samus/build_action.rb: -------------------------------------------------------------------------------- 1 | require_relative './action' 2 | 3 | module Samus 4 | class BuildAction < Action 5 | def initialize(opts = {}) 6 | super(opts) 7 | @pwd = nil 8 | @skip = false 9 | end 10 | 11 | attr_reader :publish 12 | 13 | def stage 14 | 'build' 15 | end 16 | 17 | def command_options 18 | super.merge(pwd: @pwd) 19 | end 20 | 21 | attr_writer :pwd 22 | 23 | def run 24 | return if @skip 25 | super 26 | end 27 | 28 | def publish=(publish) 29 | @publish = publish.is_a?(Array) ? publish : [publish] 30 | @publish.each do |publish_action| 31 | publish_action['files'] ||= @files if @files 32 | end 33 | end 34 | 35 | attr_reader :skip 36 | def condition=(condition) 37 | @skip = !eval(condition) 38 | rescue StandardError => e 39 | puts "[E] Condition failed on #{@raw_options['action']}" 40 | raise e 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/samus/builder.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'tmpdir' 3 | require 'pathname' 4 | 5 | require_relative './build_action' 6 | 7 | module Samus 8 | class Builder 9 | RESTORE_FILE = '.git/samus-restore'.freeze 10 | 11 | class << self 12 | attr_accessor :build_version 13 | end 14 | 15 | attr_reader :build_manifest 16 | 17 | def initialize(build_manifest_file) 18 | fdata = File.read(build_manifest_file).gsub('$version', version) 19 | @stage = 'build' 20 | @build_manifest_file = build_manifest_file 21 | @build_manifest = JSON.parse(fdata) 22 | @manifest = {} 23 | end 24 | 25 | def build(dry_run = false, zip_release = true, outfile = nil, skip_restore = false) 26 | orig_pwd = Dir.pwd 27 | manifest = { 'version' => version, 'actions' => [] } 28 | build_branch = "samus-release/v#{version}" 29 | orig_branch = `git symbolic-ref -q --short HEAD`.chomp 30 | 31 | if `git diff --shortstat 2> #{devnull} | tail -n1` != '' 32 | Samus.error 'Repository is dirty, it is too dangerous to continue.' 33 | end 34 | 35 | system "git checkout -qb #{build_branch} 2>#{devnull}" 36 | remove_restore_file 37 | 38 | Dir.mktmpdir do |build_dir| 39 | pwdpath = Pathname.new(Dir.pwd) 40 | build_dir = Pathname.new(build_dir).relative_path_from(pwdpath).to_s 41 | actions.map do |action| 42 | BuildAction.new(dry_run: dry_run, arguments: { 43 | '_RESTORE_FILE' => RESTORE_FILE, 44 | '_BUILD_DIR' => build_dir, 45 | '_BUILD_BRANCH' => build_branch, 46 | '_ORIG_BRANCH' => orig_branch, 47 | '_DEVNULL' => devnull, 48 | 'VERSION' => version 49 | }).load(action) 50 | end.each do |action| 51 | next if action.skip 52 | action.run 53 | manifest['actions'] += action.publish if action.publish 54 | end 55 | 56 | unless dry_run 57 | Dir.chdir(build_dir) do 58 | generate_manifest(manifest) 59 | generate_release(orig_pwd, zip_release, outfile) 60 | end 61 | end 62 | end 63 | ensure 64 | if skip_restore 65 | remove_restore_file 66 | else 67 | restore_git_repo 68 | system "git checkout -q #{orig_branch} 2>#{devnull}" 69 | system "git branch -qD #{build_branch} 2>#{devnull}" 70 | end 71 | end 72 | 73 | private 74 | 75 | def generate_manifest(manifest) 76 | File.open('manifest.json', 'w') do |f| 77 | f.puts JSON.pretty_generate(manifest, indent: ' ') 78 | end 79 | end 80 | 81 | def generate_release(orig_pwd, zip_release = true, outfile = nil) 82 | file = outfile || build_manifest['output'] || "release-v#{version}" 83 | file = File.join(orig_pwd, file) unless file[0] == '/' 84 | file_is_zipped = file =~ /\.(tar\.gz|tgz)$/ 85 | if zip_release || file_is_zipped 86 | file += '.tar.gz' unless file_is_zipped 87 | system "tar cfz #{file.inspect} *" 88 | else 89 | system "mkdir #{file.inspect} && cp -R * #{file.inspect}" 90 | end 91 | Samus.error "Failed to build release package" if $?.to_i != 0 92 | puts "[I] Built release package: #{File.basename(file)}" 93 | end 94 | 95 | def actions 96 | build_manifest['actions'] 97 | end 98 | 99 | def restore_git_repo 100 | return unless File.file?(RESTORE_FILE) 101 | 102 | File.readlines(RESTORE_FILE).each do |line| 103 | type, branch, commit = *line.split(/\s+/) 104 | case type 105 | when 'tag' 106 | puts "[D] Removing tag #{branch}" if $DEBUG 107 | system "git tag -d #{branch} >#{devnull}" 108 | when 'branch' 109 | puts "[D] Restoring #{branch} to #{commit}" if $DEBUG 110 | system "git checkout -q #{branch}" 111 | system "git reset -q --hard #{commit}" 112 | end 113 | end 114 | ensure 115 | remove_restore_file 116 | end 117 | 118 | def remove_restore_file 119 | File.unlink(RESTORE_FILE) if File.file?(RESTORE_FILE) 120 | end 121 | 122 | def version 123 | self.class.build_version 124 | end 125 | 126 | def devnull 127 | Samus.windows? ? 'NUL' : '/dev/null' 128 | end 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/samus/command.rb: -------------------------------------------------------------------------------- 1 | module Samus 2 | class Command 3 | class << self 4 | attr_reader :command_paths 5 | 6 | def list_commands(stage = nil) 7 | display_commands(collect_commands(stage)) 8 | end 9 | 10 | private 11 | 12 | def display_commands(stages) 13 | puts 'Commands:' 14 | puts '' 15 | stages.each do |type, commands| 16 | puts "#{type}:" 17 | puts '' 18 | commands.sort.each do |command| 19 | puts(format(' * %<name>-20s%<desc>s', 20 | name: command.name, 21 | desc: command.help_text.split(/\r?\n/)[0])) 22 | end 23 | puts '' 24 | end 25 | end 26 | 27 | def collect_commands(stage) 28 | stages = {} 29 | command_paths.each do |path| 30 | Dir.glob(File.join(path, '*', '*')).each do |dir| 31 | type, name = *dir.split(File::SEPARATOR)[-2, 2] 32 | next if name =~ /\.md$/ 33 | next if stage && stage != type 34 | (stages[type] ||= []).push(new(type, name)) 35 | end 36 | end 37 | stages 38 | end 39 | end 40 | 41 | @command_paths = [File.expand_path( 42 | File.join(File.dirname(__FILE__), '..', '..', 'commands') 43 | )] 44 | 45 | attr_reader :stage, :name 46 | 47 | def initialize(stage, name) 48 | @name = name 49 | @stage = stage 50 | load_full_path 51 | end 52 | 53 | def show_help 54 | puts "#{stage.capitalize} Command: #{name}" 55 | puts '' 56 | puts help_text 57 | end 58 | 59 | def help_text 60 | @help_text ||= File.exist?(help_path) ? File.read(help_path) : '' 61 | end 62 | 63 | def log_command(env = {}, arguments = []) 64 | e = env.map { |k, v| k =~ /^(AWS|__)/ ? nil : "#{k}=#{v.inspect}" }.compact.join(' ') 65 | e += ' ' unless e.empty? 66 | puts('[C] ' + e + name + (arguments ? ' ' + arguments.join(' ') : '')) 67 | end 68 | 69 | def run(opts = {}) 70 | env = (opts[:arguments] || {}).each_with_object({}) { |(k, v), h| h["_#{k.upcase}"] = v; } 71 | arguments = opts[:files] || [] 72 | dry_run = opts[:dry_run] || false 73 | allow_fail = opts[:allow_fail] || false 74 | pwd = opts[:pwd] 75 | 76 | log_command(env, arguments) 77 | 78 | return if dry_run 79 | exec_in_dir(pwd) do 80 | system(env, exe_type + @full_path + ' ' + (arguments ? arguments.join(' ') : '')) 81 | end 82 | report_error($?, allow_fail) 83 | end 84 | 85 | def <=>(other) 86 | name <=> other.name 87 | end 88 | 89 | private 90 | 91 | def report_error(exit_code, allow_fail) 92 | return if exit_code.to_i.zero? 93 | puts "[E] Last command failed with #{exit_code}#{allow_fail ? ' but allowFail=true' : ', exiting'}." 94 | exit(exit_code.to_i) unless allow_fail 95 | end 96 | 97 | def exec_in_dir(dir, &block) 98 | dir ? Dir.chdir(dir, &block) : yield 99 | end 100 | 101 | def exe_type 102 | return '' unless Samus.windows? 103 | 104 | if File.readlines(@full_path).first.chomp =~ /^#!(.+)/ 105 | path = $1 106 | if path == '/bin/sh' 107 | 'sh ' 108 | elsif path == '/usr/bin/env ruby' 109 | 'ruby ' 110 | else 111 | path + ' ' 112 | end 113 | end 114 | end 115 | 116 | def load_full_path 117 | path = self.class.command_paths.find do |ipath| 118 | File.exist?(File.join(ipath, stage, name)) 119 | end 120 | 121 | if path 122 | @full_path = File.join(path, stage, name) 123 | else 124 | Samus.error "Could not find command: #{name} " \ 125 | "(cmd_paths=#{self.class.command_paths.join(':')})" 126 | end 127 | end 128 | 129 | def help_path 130 | @help_path ||= @full_path + '.help.md' 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/samus/credentials.rb: -------------------------------------------------------------------------------- 1 | module Samus 2 | class Credentials 3 | attr_reader :name 4 | 5 | class << self 6 | attr_accessor :credentials 7 | end 8 | 9 | @credentials = {} 10 | 11 | def initialize(name) 12 | @name = name 13 | load_credential_file 14 | end 15 | 16 | def load 17 | return self.class.credentials[name] if self.class.credentials[name] 18 | 19 | hsh = {} 20 | data = nil 21 | if File.executable?(@file) 22 | data = `#{@file}` 23 | if $?.to_i != 0 24 | Samus.error "Loading credential #{name} failed with #{$?}" 25 | end 26 | else 27 | data = File.read(@file) 28 | end 29 | 30 | data.split(/\r?\n/).each do |line| 31 | name, value = *line.strip.split(':') 32 | if value.nil? 33 | Samus.error "Failed to parse credential from #{@file} (exec bit: #{File.executable?(@file)})" 34 | end 35 | 36 | hsh["_creds_#{name.strip.downcase}"] = value.strip 37 | end 38 | 39 | self.class.credentials[name] = hsh 40 | end 41 | 42 | private 43 | 44 | def load_credential_file 45 | Samus.config_paths.each do |path| 46 | file = File.join(path, 'credentials', name) 47 | if File.exist?(file) 48 | @file = file 49 | return 50 | end 51 | end 52 | Samus.error "Could not find credential: #{name} " \ 53 | "(SAMUS_CONFIG_PATH=#{Samus.config_paths.join(':')})" 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/samus/publish_action.rb: -------------------------------------------------------------------------------- 1 | require_relative './action' 2 | 3 | module Samus 4 | class PublishAction < Action 5 | def stage 6 | 'publish' 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/samus/publisher.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | require_relative './publish_action' 4 | 5 | module Samus 6 | class Publisher 7 | def initialize(dir) 8 | @dir = dir 9 | @stage = 'publish' 10 | end 11 | 12 | def publish(dry_run = false) 13 | Dir.chdir(@dir) do 14 | actions.map do |action| 15 | PublishAction.new( 16 | dry_run: dry_run, 17 | arguments: { 'VERSION' => manifest['version'] } 18 | ).load(action) 19 | end.each(&:run) 20 | end 21 | end 22 | 23 | private 24 | 25 | def actions 26 | manifest['actions'] 27 | end 28 | 29 | def manifest 30 | @manifest ||= JSON.parse(File.read(manifest_file)) 31 | end 32 | 33 | def manifest_file 34 | @manifest_file ||= File.join(@dir, 'manifest.json') 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/samus/rake/tasks.rb: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/tasklib' 3 | require 'tempfile' 4 | require 'fileutils' 5 | 6 | module Samus 7 | module Rake 8 | module Helpers 9 | private 10 | 11 | def release_version 12 | return @version if defined?(@version) 13 | raise "Missing VERSION=X.Y.Z" unless ENV['VERSION'] 14 | @version = ENV['VERSION'].sub(/^v/, '') 15 | end 16 | 17 | def release_image 18 | return @image if defined?(@image) 19 | @image = "samus/release/#{File.basename(Dir.pwd)}:v#{release_version}" 20 | end 21 | end 22 | 23 | class DockerReleaseTask < ::Rake::TaskLib 24 | include Helpers 25 | 26 | PREP_DIR = '.samusprep' 27 | DEFAULT_DOCKERFILE = "Dockerfile.samus" 28 | 29 | attr_accessor :dockerfile 30 | 31 | attr_accessor :delete_image_after_publish 32 | 33 | attr_accessor :git_pull_before_build 34 | 35 | attr_accessor :git_pull_after_publish 36 | 37 | attr_accessor :mount_samus_config 38 | 39 | attr_accessor :extra_config 40 | 41 | def initialize(namespace = :samus) 42 | @namespace = namespace 43 | @dockerfile = DEFAULT_DOCKERFILE 44 | @delete_image_after_publish = true 45 | @git_pull_before_build = true 46 | @git_pull_after_publish = true 47 | @mount_samus_config = false 48 | @extra_config = {} 49 | 50 | yield self if block_given? 51 | 52 | build_config_files 53 | define 54 | end 55 | 56 | private 57 | 58 | def build_config_files 59 | @config_files = { 60 | Samus::CONFIG_PATH => '.samus', 61 | File.expand_path('~/.gitconfig') => '.gitconfig' 62 | }.merge(extra_config) 63 | @config_files.select! {|src, _dst| File.exist?(src) } 64 | end 65 | 66 | def copy_prep 67 | FileUtils.rm_rf(PREP_DIR) 68 | FileUtils.mkdir_p(PREP_DIR) 69 | @config_files.each do |src, dst| 70 | FileUtils.cp_r(src, File.join(PREP_DIR, dst)) 71 | end 72 | end 73 | 74 | def build_or_get_dockerfile 75 | return dockerfile if File.exist?(dockerfile) 76 | fname = File.join(PREP_DIR, 'Dockerfile') 77 | config_copies = @config_files.values.map {|f| "COPY ./#{PREP_DIR}/#{f} /root/#{f}" } 78 | File.open(fname, 'w') do |f| 79 | f.puts([ 80 | "FROM lsegal/samus:build", 81 | "ARG VERSION", 82 | "ENV VERSION=${VERSION}", 83 | "COPY . /build", 84 | config_copies.join("\n"), 85 | "RUN rm -rf /build/.samusprep", 86 | "RUN samus build --skip-restore ${VERSION}" 87 | ].join("\n")) 88 | end 89 | fname 90 | end 91 | 92 | def define 93 | namespace(@namespace) do 94 | desc '[VERSION=X.Y.Z] Builds a Samus release using Docker' 95 | task :build do 96 | img = release_image 97 | ver = release_version 98 | sh "git pull" if git_pull_before_build 99 | 100 | begin 101 | copy_prep 102 | sh "docker build . --rm -t #{img} -f #{build_or_get_dockerfile} --build-arg VERSION=#{ver}" 103 | ensure 104 | FileUtils.rm_rf(PREP_DIR) 105 | end 106 | end 107 | 108 | desc '[VERSION=X.Y.Z] Publishes a built release using Docker' 109 | task :publish do 110 | img = release_image 111 | mount = mount_samus_config ? "-v #{Samus::CONFIG_PATH}:/root/.samus:ro" : '' 112 | sh "docker run #{mount} --rm #{release_image}" 113 | ::Rake::Task["#{@namespace}:clean"].execute if delete_image_after_publish 114 | sh "git pull" if git_pull_after_publish 115 | end 116 | 117 | desc '[VERSION=X.Y.Z] Inspects a built release using Docker shell' 118 | task :inspect do 119 | sh "docker run -it --entrypoint sh #{release_image}" 120 | end 121 | 122 | desc '[VERSION=X.Y.Z] Removes a built release using Docker' 123 | task :clean do 124 | sh "docker rmi -f #{release_image}" 125 | end 126 | end 127 | end 128 | end 129 | 130 | class ReleaseTask < ::Rake::TaskLib 131 | include Helpers 132 | 133 | attr_accessor :git_pull_before_build 134 | attr_accessor :git_pull_after_publish 135 | attr_accessor :buildfile 136 | attr_writer :zipfile 137 | 138 | def initialize(namespace = :samus) 139 | @namespace = namespace 140 | @buildfile = "" 141 | @zipfile = nil 142 | @git_pull_before_build = true 143 | @git_pull_after_publish = true 144 | yield self if block_given? 145 | define 146 | end 147 | 148 | private 149 | 150 | def zipfile 151 | @zipfile || "release-v#{release_version}.tar.gz" 152 | end 153 | 154 | def define 155 | namespace(@namespace) do 156 | desc '[VERSION=X.Y.Z] Builds a Samus release' 157 | task :build do 158 | sh "git pull" if git_pull_before_build 159 | sh "samus build -o #{zipfile} #{release_version} #{buildfile}" 160 | end 161 | 162 | desc '[VERSION=X.Y.Z] Publishes a built release' 163 | task :publish do 164 | sh "samus publish #{zipfile}" 165 | sh "git pull" if git_pull_after_publish 166 | end 167 | end 168 | end 169 | end 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /lib/samus/version.rb: -------------------------------------------------------------------------------- 1 | module Samus 2 | VERSION = '3.0.9'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /samus.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path('../lib/samus/version', __FILE__) 2 | 3 | Gem::Specification.new do |s| 4 | s.name = 'samus' 5 | s.summary = 'Samus helps you release Open Source Software.' 6 | s.version = Samus::VERSION 7 | s.author = 'Loren Segal' 8 | s.email = 'lsegal@soen.ca' 9 | s.homepage = 'http://github.com/lsegal/samus' 10 | s.files = `git ls-files`.split(/\s+/) 11 | s.executables = ['samus'] 12 | s.license = 'MIT' 13 | end 14 | -------------------------------------------------------------------------------- /samus.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [ 3 | { 4 | "action": "fs-sedfiles", 5 | "files": [ 6 | "lib/*/version.rb" 7 | ], 8 | "arguments": { 9 | "search": "VERSION = ['\"](.+?)['\"]", 10 | "replace": "VERSION = '$version'" 11 | } 12 | }, 13 | { 14 | "action": "changelog-rotate", 15 | "files": [ 16 | "CHANGELOG.md" 17 | ], 18 | "arguments": { 19 | "title_format": "$version - %B %-d$day_nth, %Y", 20 | "tz": "GMT+8" 21 | } 22 | }, 23 | { 24 | "action": "chmod-files", 25 | "files": [ 26 | "**/*,644", 27 | "bin/*,755", 28 | "commands/**/*,755", 29 | "commands/**/*.md,644" 30 | ] 31 | }, 32 | { 33 | "action": "git-commit", 34 | "files": [ 35 | "CHANGELOG.md", 36 | "lib/*/version.rb" 37 | ] 38 | }, 39 | { 40 | "action": "git-merge", 41 | "arguments": { 42 | "branch": "main" 43 | } 44 | }, 45 | { 46 | "action": "git-clone", 47 | "files": [ 48 | "git-repo" 49 | ], 50 | "publish": [ 51 | { 52 | "action": "git-push", 53 | "credentials": "lsegal.github.ssh", 54 | "arguments": { 55 | "remotes": "origin", 56 | "refs": "main v$version" 57 | } 58 | } 59 | ] 60 | }, 61 | { 62 | "action": "gem-build", 63 | "files": [ 64 | "*.gemspec" 65 | ], 66 | "publish": [ 67 | { 68 | "action": "gem-push", 69 | "files": [ 70 | "*.gem" 71 | ], 72 | "credentials": "lsegal.rubygems" 73 | } 74 | ] 75 | }, 76 | { 77 | "action": "changelog-parse", 78 | "files": [ 79 | "CHANGELOG.md" 80 | ], 81 | "publish": [ 82 | { 83 | "action": "github-release", 84 | "credentials": "lsegal.github", 85 | "files": [], 86 | "arguments": { 87 | "repository": "lsegal/samus", 88 | "tag": "v$version", 89 | "changelog": "CHANGELOG.md" 90 | } 91 | } 92 | ] 93 | } 94 | ] 95 | } 96 | --------------------------------------------------------------------------------