├── 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
[](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(?.*?)\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(?.*?)(?\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(' * %-20s%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 |
--------------------------------------------------------------------------------